tuile 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +15 -0
- data/README.md +51 -24
- data/book/01-first-app.md +186 -0
- data/book/02-repaint.md +177 -0
- data/book/03-layout.md +379 -0
- data/book/04-event-loop.md +209 -0
- data/book/05-focus.md +188 -0
- data/book/06-theming.md +250 -0
- data/book/07-components.md +231 -0
- data/book/08-testing.md +186 -0
- data/book/README.md +79 -0
- data/examples/hello_world.rb +1 -2
- data/lib/tuile/buffer.rb +100 -31
- data/lib/tuile/component/button.rb +1 -9
- data/lib/tuile/component/info_window.rb +4 -2
- data/lib/tuile/component/label.rb +7 -15
- data/lib/tuile/component/layout.rb +0 -9
- data/lib/tuile/component/list.rb +0 -26
- data/lib/tuile/component/log_window.rb +0 -14
- data/lib/tuile/component/popup.rb +70 -75
- data/lib/tuile/component/text_view.rb +0 -23
- data/lib/tuile/component/window.rb +57 -75
- data/lib/tuile/component.rb +1 -60
- data/lib/tuile/event_queue.rb +31 -10
- data/lib/tuile/fake_event_queue.rb +21 -5
- data/lib/tuile/fraction.rb +46 -0
- data/lib/tuile/screen.rb +8 -5
- data/lib/tuile/screen_pane.rb +5 -3
- data/lib/tuile/version.rb +1 -1
- data/sig/tuile.rbs +195 -223
- metadata +12 -4
- data/ideas/back-buffer.md +0 -217
- data/lib/tuile/sizing.rb +0 -59
|
@@ -33,23 +33,39 @@ module Tuile
|
|
|
33
33
|
def post(event); end
|
|
34
34
|
|
|
35
35
|
# Mirrors {EventQueue#tick} but timeless: returns a {FakeTicker} that
|
|
36
|
-
# only fires when a test calls {#tick_once}. The `
|
|
36
|
+
# only fires when a test calls {#tick_once}. The `seconds` argument is
|
|
37
37
|
# validated the same way the real queue validates it, then discarded —
|
|
38
38
|
# the fake has no clock, so frame cadence is up to the test.
|
|
39
39
|
#
|
|
40
|
-
# @param
|
|
41
|
-
# for parity with {EventQueue#tick}; otherwise unused.
|
|
40
|
+
# @param seconds [Numeric] interval between firings, must be positive.
|
|
41
|
+
# Validated for parity with {EventQueue#tick}; otherwise unused.
|
|
42
42
|
# @yield [tick] called on each {#tick_once}.
|
|
43
43
|
# @yieldparam tick [Integer] 0-based monotonically increasing counter.
|
|
44
44
|
# @yieldreturn [void]
|
|
45
45
|
# @return [FakeTicker]
|
|
46
|
-
def tick(
|
|
46
|
+
def tick(seconds, &block)
|
|
47
|
+
raise ArgumentError, "block required" unless block
|
|
48
|
+
unless seconds.is_a?(Numeric) && seconds.positive?
|
|
49
|
+
raise ArgumentError, "seconds must be a positive Numeric, got #{seconds.inspect}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
FakeTicker.new(block).tap { |t| @tickers << t }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Mirrors {EventQueue#tick_fps}: validates `fps` for parity, then delegates
|
|
56
|
+
# to {#tick} (the fake discards the interval regardless).
|
|
57
|
+
# @param fps [Numeric] firings per second, must be positive.
|
|
58
|
+
# @yield [tick] called on each {#tick_once}.
|
|
59
|
+
# @yieldparam tick [Integer] 0-based monotonically increasing counter.
|
|
60
|
+
# @yieldreturn [void]
|
|
61
|
+
# @return [FakeTicker]
|
|
62
|
+
def tick_fps(fps, &block)
|
|
47
63
|
raise ArgumentError, "block required" unless block
|
|
48
64
|
unless fps.is_a?(Numeric) && fps.positive?
|
|
49
65
|
raise ArgumentError, "fps must be a positive Numeric, got #{fps.inspect}"
|
|
50
66
|
end
|
|
51
67
|
|
|
52
|
-
|
|
68
|
+
tick(1.0 / fps, &block)
|
|
53
69
|
end
|
|
54
70
|
|
|
55
71
|
# Test helper: fires every live ticker's user block once and prunes
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tuile
|
|
4
|
+
# A width/height ratio, each a float in `0.0..1.0` — the single relational
|
|
5
|
+
# sizing primitive in Tuile. It exists for exactly one job: sizing a
|
|
6
|
+
# {Component::Popup} against the screen. A popup has no siblings competing for
|
|
7
|
+
# space and no rectangle in a tiled layout, so "half the screen, centered" is
|
|
8
|
+
# the sensible default, and that wants a ratio rather than a hard-coded cell
|
|
9
|
+
# count that would be wrong on the next terminal size.
|
|
10
|
+
#
|
|
11
|
+
# Tiled components are *not* sized this way: their parent computes explicit
|
|
12
|
+
# integer rects in its own `rect=` and hands them down. `Fraction` is
|
|
13
|
+
# deliberately scoped to {Component::Popup#size=} and is not a general layout
|
|
14
|
+
# primitive.
|
|
15
|
+
#
|
|
16
|
+
# Resolve it against a reference {Size} (the screen) to get concrete integer
|
|
17
|
+
# cells:
|
|
18
|
+
#
|
|
19
|
+
# Fraction::HALF.resolve(Size.new(80, 24)) # => 40x12
|
|
20
|
+
#
|
|
21
|
+
# Integer arguments are coerced to float, so `Fraction.new(1, 1) == FULL`.
|
|
22
|
+
class Fraction < Data.define(:width, :height)
|
|
23
|
+
# @param width [Numeric] fraction of the reference width, `0.0..1.0`.
|
|
24
|
+
# @param height [Numeric] fraction of the reference height, `0.0..1.0`.
|
|
25
|
+
def initialize(width:, height:)
|
|
26
|
+
super(width: width.to_f, height: height.to_f)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Resolves this fraction against a reference size, rounding each axis to the
|
|
30
|
+
# nearest cell and flooring at 1 — so a fraction never yields a zero-size
|
|
31
|
+
# result on a tiny terminal.
|
|
32
|
+
# @param reference [Size] the size to take a fraction of (usually the screen).
|
|
33
|
+
# @return [Size]
|
|
34
|
+
def resolve(reference)
|
|
35
|
+
Size.new([(reference.width * width).round, 1].max,
|
|
36
|
+
[(reference.height * height).round, 1].max)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Half the reference size on each axis — the default {Component::Popup} size.
|
|
40
|
+
# @return [Fraction]
|
|
41
|
+
HALF = new(0.5, 0.5)
|
|
42
|
+
# The full reference size on each axis — fullscreen for a {Component::Popup}.
|
|
43
|
+
# @return [Fraction]
|
|
44
|
+
FULL = new(1.0, 1.0)
|
|
45
|
+
end
|
|
46
|
+
end
|
data/lib/tuile/screen.rb
CHANGED
|
@@ -35,9 +35,9 @@ module Tuile
|
|
|
35
35
|
@repainting = Set.new
|
|
36
36
|
# Until the event loop is run, we pretend we're in the UI thread.
|
|
37
37
|
@pretend_ui_lock = true
|
|
38
|
-
@
|
|
38
|
+
@color_scheme = detect_scheme
|
|
39
39
|
@theme_def = ThemeDef.default
|
|
40
|
-
@theme = @theme_def.for(@
|
|
40
|
+
@theme = @theme_def.for(@color_scheme)
|
|
41
41
|
# Structural root of the component tree: holds tiled content, popup
|
|
42
42
|
# stack and status bar.
|
|
43
43
|
@pane = ScreenPane.new
|
|
@@ -60,6 +60,9 @@ module Tuile
|
|
|
60
60
|
# @return [ScreenPane] the structural root of the component tree.
|
|
61
61
|
attr_reader :pane
|
|
62
62
|
|
|
63
|
+
# @return [Symbol] `:light` or `:dark`
|
|
64
|
+
attr_reader :color_scheme
|
|
65
|
+
|
|
63
66
|
# @return [Buffer] the back buffer components paint into
|
|
64
67
|
# ({Buffer#set_line} / {Buffer#fill} / {Buffer#set_char}).
|
|
65
68
|
attr_reader :buffer
|
|
@@ -133,7 +136,7 @@ module Tuile
|
|
|
133
136
|
|
|
134
137
|
check_locked
|
|
135
138
|
@theme_def = theme_def
|
|
136
|
-
self.theme = @theme_def.for(@
|
|
139
|
+
self.theme = @theme_def.for(@color_scheme)
|
|
137
140
|
end
|
|
138
141
|
|
|
139
142
|
# Replaces the theme and restyles the whole UI: fires
|
|
@@ -565,8 +568,8 @@ module Tuile
|
|
|
565
568
|
# @param scheme [Symbol] `:dark` or `:light`.
|
|
566
569
|
# @return [void]
|
|
567
570
|
def on_color_scheme(scheme)
|
|
568
|
-
@
|
|
569
|
-
self.theme = @theme_def.for(@
|
|
571
|
+
@color_scheme = scheme
|
|
572
|
+
self.theme = @theme_def.for(@color_scheme)
|
|
570
573
|
end
|
|
571
574
|
|
|
572
575
|
# Walks the current modal scope in pre-order, collects tab stops, and
|
data/lib/tuile/screen_pane.rb
CHANGED
|
@@ -123,14 +123,16 @@ module Tuile
|
|
|
123
123
|
end
|
|
124
124
|
|
|
125
125
|
# Lays out content (full pane minus the bottom row) and the status bar
|
|
126
|
-
# (bottom row).
|
|
127
|
-
#
|
|
126
|
+
# (bottom row). Each popup re-resolves its {Component::Popup#size} against
|
|
127
|
+
# the new screen via {Component::Popup#reposition} — so a {Fraction} size
|
|
128
|
+
# tracks resize — repositioning itself (modal popups recenter; non-modal
|
|
129
|
+
# overlays keep the top-left their owner assigned).
|
|
128
130
|
# @return [void]
|
|
129
131
|
def layout
|
|
130
132
|
return if rect.empty?
|
|
131
133
|
|
|
132
134
|
@content.rect = Rect.new(rect.left, rect.top, rect.width, [rect.height - 1, 0].max) unless @content.nil?
|
|
133
|
-
@popups.each
|
|
135
|
+
@popups.each(&:reposition)
|
|
134
136
|
@status_bar.rect = Rect.new(rect.left, rect.top + rect.height - 1, rect.width, 1)
|
|
135
137
|
end
|
|
136
138
|
|
data/lib/tuile/version.rb
CHANGED