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.
@@ -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 `fps` argument is
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 fps [Numeric] firings per second, must be positive. Validated
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(fps, &block)
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
- FakeTicker.new(block).tap { |t| @tickers << t }
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
- @scheme = detect_scheme
38
+ @color_scheme = detect_scheme
39
39
  @theme_def = ThemeDef.default
40
- @theme = @theme_def.for(@scheme)
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(@scheme)
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
- @scheme = scheme
569
- self.theme = @theme_def.for(@scheme)
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
@@ -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). Modal popups self-recenter via {Component::Popup#center};
127
- # non-modal overlays keep the position their owner assigned.
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 { |p| p.center if p.modal? }
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
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Tuile
4
4
  # @return [String]
5
- VERSION = "0.8.0"
5
+ VERSION = "0.9.0"
6
6
  end