zaniah 0.2.0 → 0.3.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 +13 -0
- data/LICENSE.txt +18 -0
- data/README.md +14 -4
- data/assets/icons/check.svg +1 -0
- data/assets/icons/close.svg +1 -0
- data/assets/icons/info.svg +1 -0
- data/assets/icons/menu.svg +1 -0
- data/assets/icons/search.svg +1 -0
- data/assets/icons/warning.svg +1 -0
- data/docs/accessibility.md +61 -0
- data/docs/adr/004-style-resolution.md +17 -0
- data/docs/adr/005-instance-layout.md +43 -0
- data/docs/adr/006-component-model.md +17 -0
- data/docs/adr/007-text-buffer.md +17 -0
- data/docs/adr/008-accessibility-bridge.md +19 -0
- data/docs/adr/009-grid-scope.md +17 -0
- data/docs/animation.md +46 -0
- data/docs/components.md +109 -0
- data/docs/gallery/command-palette-dark.png +0 -0
- data/docs/gallery/command-palette-high_contrast.png +0 -0
- data/docs/gallery/command-palette-light.png +0 -0
- data/docs/gallery/components-dark.png +0 -0
- data/docs/gallery/components-high_contrast.png +0 -0
- data/docs/gallery/components-light.png +0 -0
- data/docs/gallery/context-menu-dark.png +0 -0
- data/docs/gallery/context-menu-high_contrast.png +0 -0
- data/docs/gallery/context-menu-light.png +0 -0
- data/docs/gallery/dialog-dark.png +0 -0
- data/docs/gallery/dialog-high_contrast.png +0 -0
- data/docs/gallery/dialog-light.png +0 -0
- data/docs/gallery/drawer-left-dark.png +0 -0
- data/docs/gallery/drawer-left-high_contrast.png +0 -0
- data/docs/gallery/drawer-left-light.png +0 -0
- data/docs/gallery/drawer-right-dark.png +0 -0
- data/docs/gallery/drawer-right-high_contrast.png +0 -0
- data/docs/gallery/drawer-right-light.png +0 -0
- data/docs/gallery/menu-dark.png +0 -0
- data/docs/gallery/menu-high_contrast.png +0 -0
- data/docs/gallery/menu-light.png +0 -0
- data/docs/gallery/modal-dark.png +0 -0
- data/docs/gallery/modal-high_contrast.png +0 -0
- data/docs/gallery/modal-light.png +0 -0
- data/docs/gallery/popover-bottom-dark.png +0 -0
- data/docs/gallery/popover-bottom-high_contrast.png +0 -0
- data/docs/gallery/popover-bottom-light.png +0 -0
- data/docs/gallery/popover-left-dark.png +0 -0
- data/docs/gallery/popover-left-high_contrast.png +0 -0
- data/docs/gallery/popover-left-light.png +0 -0
- data/docs/gallery/popover-right-dark.png +0 -0
- data/docs/gallery/popover-right-high_contrast.png +0 -0
- data/docs/gallery/popover-right-light.png +0 -0
- data/docs/gallery/popover-top-dark.png +0 -0
- data/docs/gallery/popover-top-high_contrast.png +0 -0
- data/docs/gallery/popover-top-light.png +0 -0
- data/docs/gallery/toast-danger-dark.png +0 -0
- data/docs/gallery/toast-danger-high_contrast.png +0 -0
- data/docs/gallery/toast-danger-light.png +0 -0
- data/docs/gallery/toast-info-dark.png +0 -0
- data/docs/gallery/toast-info-high_contrast.png +0 -0
- data/docs/gallery/toast-info-light.png +0 -0
- data/docs/gallery/toast-success-dark.png +0 -0
- data/docs/gallery/toast-success-high_contrast.png +0 -0
- data/docs/gallery/toast-success-light.png +0 -0
- data/docs/gallery/toast-warning-dark.png +0 -0
- data/docs/gallery/toast-warning-high_contrast.png +0 -0
- data/docs/gallery/toast-warning-light.png +0 -0
- data/docs/gallery/tooltip-bottom-dark.png +0 -0
- data/docs/gallery/tooltip-bottom-high_contrast.png +0 -0
- data/docs/gallery/tooltip-bottom-light.png +0 -0
- data/docs/gallery/tooltip-left-dark.png +0 -0
- data/docs/gallery/tooltip-left-high_contrast.png +0 -0
- data/docs/gallery/tooltip-left-light.png +0 -0
- data/docs/gallery/tooltip-right-dark.png +0 -0
- data/docs/gallery/tooltip-right-high_contrast.png +0 -0
- data/docs/gallery/tooltip-right-light.png +0 -0
- data/docs/gallery/tooltip-top-dark.png +0 -0
- data/docs/gallery/tooltip-top-high_contrast.png +0 -0
- data/docs/gallery/tooltip-top-light.png +0 -0
- data/docs/index.html +74 -0
- data/docs/layout.md +31 -0
- data/docs/native.md +7 -2
- data/docs/site.css +32 -0
- data/docs/text.md +35 -0
- data/docs/theme.md +50 -0
- data/docs/tui.md +36 -0
- data/examples/gallery.rb +155 -0
- data/examples/native_ime.rb +13 -6
- data/lib/zaniah/accessibility/linux/service.rb +361 -0
- data/lib/zaniah/accessibility/linux.rb +23 -0
- data/lib/zaniah/accessibility/mac.rb +136 -0
- data/lib/zaniah/accessibility/native_tree.rb +53 -0
- data/lib/zaniah/accessibility/node.rb +12 -0
- data/lib/zaniah/accessibility/tree.rb +61 -0
- data/lib/zaniah/accessibility/windows/provider.rb +273 -0
- data/lib/zaniah/accessibility/windows.rb +38 -0
- data/lib/zaniah/accessibility.rb +63 -0
- data/lib/zaniah/animation/animator.rb +95 -0
- data/lib/zaniah/animation/easing.rb +45 -0
- data/lib/zaniah/animation/spring.rb +36 -0
- data/lib/zaniah/animation/transition.rb +5 -0
- data/lib/zaniah/animation.rb +6 -0
- data/lib/zaniah/app.rb +22 -2
- data/lib/zaniah/color.rb +16 -0
- data/lib/zaniah/devtools/hot_reload.rb +40 -0
- data/lib/zaniah/devtools/inspector.rb +111 -0
- data/lib/zaniah/devtools/stats_overlay.rb +16 -0
- data/lib/zaniah/devtools.rb +13 -0
- data/lib/zaniah/element.rb +210 -15
- data/lib/zaniah/frame_context.rb +4 -1
- data/lib/zaniah/gpu/frame_encoder.rb +10 -1
- data/lib/zaniah/gpu/instance_packing.rb +33 -5
- data/lib/zaniah/gpu/metal.rb +26 -10
- data/lib/zaniah/gpu/open_gl.rb +35 -14
- data/lib/zaniah/gpu/software.rb +116 -14
- data/lib/zaniah/gpu/vulkan/scene.frag +51 -0
- data/lib/zaniah/gpu/vulkan/scene.frag.spv +0 -0
- data/lib/zaniah/gpu/vulkan/scene.vert +50 -0
- data/lib/zaniah/gpu/vulkan/scene.vert.spv +0 -0
- data/lib/zaniah/gpu/vulkan.rb +282 -36
- data/lib/zaniah/gpu.rb +8 -3
- data/lib/zaniah/input/dispatcher.rb +96 -12
- data/lib/zaniah/input/focus_handle.rb +13 -4
- data/lib/zaniah/input/focus_tree.rb +77 -0
- data/lib/zaniah/input/keymap.rb +77 -0
- data/lib/zaniah/input.rb +1 -0
- data/lib/zaniah/interactivity.rb +30 -0
- data/lib/zaniah/layout/engine.rb +173 -36
- data/lib/zaniah/layout/grid.rb +134 -0
- data/lib/zaniah/layout/style.rb +16 -1
- data/lib/zaniah/layout.rb +1 -0
- data/lib/zaniah/length.rb +3 -0
- data/lib/zaniah/length_units.rb +7 -0
- data/lib/zaniah/list.rb +27 -15
- data/lib/zaniah/platform/appearance.rb +11 -0
- data/lib/zaniah/platform/headless/window.rb +101 -9
- data/lib/zaniah/platform/linux/appearance_aware.rb +8 -0
- data/lib/zaniah/platform/linux/wayland_window.rb +3 -1
- data/lib/zaniah/platform/linux/window.rb +4 -2
- data/lib/zaniah/platform/mac/app.rb +1 -1
- data/lib/zaniah/platform/mac/core_text.rb +3 -0
- data/lib/zaniah/platform/mac/window.rb +9 -1
- data/lib/zaniah/platform/tui/window.rb +1 -1
- data/lib/zaniah/platform/windows/window.rb +12 -2
- data/lib/zaniah/platform.rb +1 -0
- data/lib/zaniah/scene.rb +137 -28
- data/lib/zaniah/scroll_state.rb +117 -0
- data/lib/zaniah/scroll_view.rb +94 -0
- data/lib/zaniah/style/gradient.rb +28 -0
- data/lib/zaniah/style/ring.rb +13 -0
- data/lib/zaniah/style/shadow.rb +13 -0
- data/lib/zaniah/style/style_set.rb +43 -0
- data/lib/zaniah/style/transform.rb +40 -0
- data/lib/zaniah/svg.rb +19 -0
- data/lib/zaniah/text.rb +261 -6
- data/lib/zaniah/text_buffer.rb +119 -0
- data/lib/zaniah/text_selection.rb +18 -0
- data/lib/zaniah/text_system/atlas_cache.rb +1 -1
- data/lib/zaniah/text_system/fallback.rb +24 -0
- data/lib/zaniah/text_system/font_db.rb +4 -1
- data/lib/zaniah/text_system/kinsoku.rb +28 -0
- data/lib/zaniah/text_system/line_breaker.rb +54 -0
- data/lib/zaniah/text_system/paragraph.rb +99 -0
- data/lib/zaniah/text_system/renderer.rb +13 -2
- data/lib/zaniah/text_system/typesetter.rb +4 -0
- data/lib/zaniah/text_system.rb +3 -0
- data/lib/zaniah/theme.rb +39 -0
- data/lib/zaniah/ui/charts.rb +165 -0
- data/lib/zaniah/ui/choice_fields.rb +134 -0
- data/lib/zaniah/ui/component.rb +73 -0
- data/lib/zaniah/ui/controls.rb +439 -0
- data/lib/zaniah/ui/data.rb +364 -0
- data/lib/zaniah/ui/editors.rb +67 -0
- data/lib/zaniah/ui/forms.rb +131 -0
- data/lib/zaniah/ui/layout_components.rb +205 -0
- data/lib/zaniah/ui/modal.rb +129 -0
- data/lib/zaniah/ui/overlays.rb +282 -0
- data/lib/zaniah/ui/pickers.rb +169 -0
- data/lib/zaniah/ui/primitives.rb +199 -0
- data/lib/zaniah/ui/scrollbar.rb +104 -0
- data/lib/zaniah/ui/structure.rb +224 -0
- data/lib/zaniah/ui/text_inputs.rb +169 -0
- data/lib/zaniah/ui/variants.rb +23 -0
- data/lib/zaniah/ui.rb +18 -0
- data/lib/zaniah/unicode.rb +42 -0
- data/lib/zaniah/uniform_list.rb +27 -6
- data/lib/zaniah/version.rb +1 -1
- data/lib/zaniah.rb +14 -0
- data/sig/accessibility.rbs +34 -0
- data/sig/animation.rbs +54 -0
- data/sig/app.rbs +2 -1
- data/sig/devtools.rbs +37 -0
- data/sig/elements.rbs +88 -6
- data/sig/gpu.rbs +9 -2
- data/sig/input.rbs +31 -5
- data/sig/layout.rbs +2 -1
- data/sig/platform.rbs +24 -4
- data/sig/scene.rbs +15 -2
- data/sig/style.rbs +82 -0
- data/sig/text.rbs +35 -0
- data/sig/theme.rbs +64 -0
- data/sig/ui.rbs +390 -0
- data/sig/zaniah.rbs +56 -3
- data/tools/generate_component_gallery.rb +48 -0
- metadata +147 -2
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Zaniah
|
|
4
|
+
module Input
|
|
5
|
+
class FocusTree
|
|
6
|
+
def initialize
|
|
7
|
+
@roots, @traps = [], []
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def register(handle)
|
|
11
|
+
root = handle
|
|
12
|
+
root = root.parent while root.parent
|
|
13
|
+
@roots << root unless @roots.include?(root)
|
|
14
|
+
self
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def clear = @roots.clear
|
|
18
|
+
|
|
19
|
+
def next(from = nil) = move(from, 1)
|
|
20
|
+
def previous(from = nil) = move(from, -1)
|
|
21
|
+
|
|
22
|
+
def spatial(from, direction)
|
|
23
|
+
return unless from&.bounds
|
|
24
|
+
raise ArgumentError, "direction must be left, right, up, or down" unless %i[left right up down].include?(direction)
|
|
25
|
+
source = from.bounds
|
|
26
|
+
sx, sy = center(source)
|
|
27
|
+
candidates.reject { |handle| handle.equal?(from) || !handle.bounds }.select do |handle|
|
|
28
|
+
tx, ty = center(handle.bounds)
|
|
29
|
+
delta = %i[left right].include?(direction) ? tx - sx : ty - sy
|
|
30
|
+
%i[left up].include?(direction) ? delta.negative? : delta.positive?
|
|
31
|
+
end.min_by do |handle|
|
|
32
|
+
target = handle.bounds
|
|
33
|
+
tx, ty = center(target)
|
|
34
|
+
primary = %i[left right].include?(direction) ? tx - sx : ty - sy
|
|
35
|
+
cross_gap = %i[left right].include?(direction) ? interval_gap(source.y, source.bottom, target.y, target.bottom) : interval_gap(source.x, source.right, target.x, target.right)
|
|
36
|
+
[cross_gap.zero? ? 0 : 1, primary.abs, cross_gap, Math.hypot(tx - sx, ty - sy)]
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def trap(handle)
|
|
41
|
+
@traps << handle unless @traps.last.equal?(handle)
|
|
42
|
+
return handle unless block_given?
|
|
43
|
+
begin
|
|
44
|
+
yield
|
|
45
|
+
ensure
|
|
46
|
+
restore
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def restore = @traps.pop
|
|
51
|
+
def release_trap(handle) = @traps.delete(handle)
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def move(from, step)
|
|
56
|
+
items = candidates
|
|
57
|
+
return if items.empty?
|
|
58
|
+
index = items.index(from)
|
|
59
|
+
items[index ? (index + step) % items.length : (step.positive? ? 0 : -1)]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def candidates
|
|
63
|
+
list = []
|
|
64
|
+
roots = @traps.empty? ? @roots : [@traps.last]
|
|
65
|
+
visit = ->(handle) do
|
|
66
|
+
list << handle if handle.focusable && !handle.tab_index.nil?
|
|
67
|
+
handle.children.each(&visit)
|
|
68
|
+
end
|
|
69
|
+
roots.each(&visit)
|
|
70
|
+
list.each_with_index.sort_by { |(handle, index)| [handle.tab_index, index] }.map!(&:first)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def center(bounds) = [bounds.x + bounds.width / 2.0, bounds.y + bounds.height / 2.0]
|
|
74
|
+
def interval_gap(a1, a2, b1, b2) = [a1 - b2, b1 - a2, 0].max
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
data/lib/zaniah/input/keymap.rb
CHANGED
|
@@ -7,6 +7,83 @@ require_relative "context_predicate"
|
|
|
7
7
|
module Zaniah
|
|
8
8
|
module Input
|
|
9
9
|
class Keymap
|
|
10
|
+
class << self
|
|
11
|
+
def default_ui(platform: RUBY_PLATFORM, **options)
|
|
12
|
+
platform_defaults(platform, **options)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def platform_defaults(platform, **options)
|
|
16
|
+
primary = platform.to_s.match?(/darwin|mac/) ? "cmd" : "ctrl"
|
|
17
|
+
new(**options)
|
|
18
|
+
.bind("tab", :focus_next)
|
|
19
|
+
.bind("shift-tab", :focus_previous)
|
|
20
|
+
.bind("left", :focus_left, context: "!in_text_field && !in_list")
|
|
21
|
+
.bind("right", :focus_right, context: "!in_text_field && !in_list")
|
|
22
|
+
.bind("up", :focus_up, context: "!in_text_field && !in_list")
|
|
23
|
+
.bind("down", :focus_down, context: "!in_text_field && !in_list")
|
|
24
|
+
.bind("enter", :activate)
|
|
25
|
+
.bind("space", :activate, context: "!in_text_field")
|
|
26
|
+
.bind("esc", :dismiss)
|
|
27
|
+
.bind("home", :first, context: "in_list")
|
|
28
|
+
.bind("end", :last, context: "in_list")
|
|
29
|
+
.bind("pageup", :page_up, context: "in_list")
|
|
30
|
+
.bind("pagedown", :page_down, context: "in_list")
|
|
31
|
+
.bind("up", :previous_option, context: "in_list")
|
|
32
|
+
.bind("down", :next_option, context: "in_list")
|
|
33
|
+
.bind("#{primary}-a", :select_all, context: "in_text_field || in_list")
|
|
34
|
+
.bind("left", :move_left, context: "in_text_field")
|
|
35
|
+
.bind("right", :move_right, context: "in_text_field")
|
|
36
|
+
.bind("shift-left", :select_left, context: "in_text_field")
|
|
37
|
+
.bind("shift-right", :select_right, context: "in_text_field")
|
|
38
|
+
.bind("home", :line_start, context: "in_text_field")
|
|
39
|
+
.bind("end", :line_end, context: "in_text_field")
|
|
40
|
+
.bind("shift-home", :select_line_start, context: "in_text_field")
|
|
41
|
+
.bind("shift-end", :select_line_end, context: "in_text_field")
|
|
42
|
+
.bind("backspace", :delete_backward, context: "in_text_field")
|
|
43
|
+
.bind("delete", :delete_forward, context: "in_text_field")
|
|
44
|
+
.bind("enter", :insert_newline, context: "in_text_field")
|
|
45
|
+
.bind("left", :decrement, context: "in_slider")
|
|
46
|
+
.bind("down", :decrement, context: "in_slider")
|
|
47
|
+
.bind("right", :increment, context: "in_slider")
|
|
48
|
+
.bind("up", :increment, context: "in_slider")
|
|
49
|
+
.bind("home", :minimum, context: "in_slider")
|
|
50
|
+
.bind("end", :maximum, context: "in_slider")
|
|
51
|
+
.bind("pageup", :increment_page, context: "in_slider")
|
|
52
|
+
.bind("pagedown", :decrement_page, context: "in_slider")
|
|
53
|
+
.bind("up", :previous_option, context: "in_menu || in_tabs")
|
|
54
|
+
.bind("left", :previous_option, context: "in_menu || in_tabs")
|
|
55
|
+
.bind("down", :next_option, context: "in_menu || in_tabs")
|
|
56
|
+
.bind("right", :next_option, context: "in_menu || in_tabs")
|
|
57
|
+
.bind("home", :first, context: "in_menu || in_tabs")
|
|
58
|
+
.bind("end", :last, context: "in_menu || in_tabs")
|
|
59
|
+
.bind("up", :previous_option, context: "in_tree")
|
|
60
|
+
.bind("down", :next_option, context: "in_tree")
|
|
61
|
+
.bind("home", :first, context: "in_tree")
|
|
62
|
+
.bind("end", :last, context: "in_tree")
|
|
63
|
+
.bind("left", :collapse, context: "in_tree")
|
|
64
|
+
.bind("right", :expand, context: "in_tree")
|
|
65
|
+
.bind("up", :previous_option, context: "in_table")
|
|
66
|
+
.bind("down", :next_option, context: "in_table")
|
|
67
|
+
.bind("shift-up", :extend_previous, context: "in_table")
|
|
68
|
+
.bind("shift-down", :extend_next, context: "in_table")
|
|
69
|
+
.bind("home", :first, context: "in_table")
|
|
70
|
+
.bind("end", :last, context: "in_table")
|
|
71
|
+
.bind("pageup", :page_up, context: "in_table")
|
|
72
|
+
.bind("pagedown", :page_down, context: "in_table")
|
|
73
|
+
.bind("#{primary}-a", :select_all, context: "in_table")
|
|
74
|
+
.bind("up", :previous_option, context: "in_combobox")
|
|
75
|
+
.bind("down", :next_option, context: "in_combobox")
|
|
76
|
+
.bind("home", :first, context: "in_combobox")
|
|
77
|
+
.bind("end", :last, context: "in_combobox")
|
|
78
|
+
.bind("enter", :choose_option, context: "in_combobox")
|
|
79
|
+
.bind("esc", :dismiss, context: "in_combobox")
|
|
80
|
+
.bind("left", :previous_option, context: "in_chart")
|
|
81
|
+
.bind("right", :next_option, context: "in_chart")
|
|
82
|
+
.bind("home", :first, context: "in_chart")
|
|
83
|
+
.bind("end", :last, context: "in_chart")
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
10
87
|
def initialize(timeout: 1.0, clock: MONOTONIC_CLOCK)
|
|
11
88
|
@bindings, @pending, @last_time, @timeout, @clock = [], [], 0, timeout, clock
|
|
12
89
|
end
|
data/lib/zaniah/input.rb
CHANGED
|
@@ -13,5 +13,6 @@ require_relative "input/keystroke"
|
|
|
13
13
|
require_relative "input/context_predicate"
|
|
14
14
|
require_relative "input/keymap"
|
|
15
15
|
require_relative "input/focus_handle"
|
|
16
|
+
require_relative "input/focus_tree"
|
|
16
17
|
require_relative "input/dispatcher"
|
|
17
18
|
require_relative "input/action_registry"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
5
|
+
module Zaniah
|
|
6
|
+
class Interactivity
|
|
7
|
+
def initialize = @flags = {}
|
|
8
|
+
|
|
9
|
+
def resolve(dispatcher, pointer_position, pointer_down)
|
|
10
|
+
@flags.clear
|
|
11
|
+
chain = dispatcher.hover_chain(pointer_position)
|
|
12
|
+
chain.each { |owner| flags(owner) << :hover }
|
|
13
|
+
chain.each { |owner| flags(owner) << :active } if pointer_down
|
|
14
|
+
owner = dispatcher.focused&.owner
|
|
15
|
+
flags(owner) << :focus if owner
|
|
16
|
+
flags(owner) << :focus_visible if owner && dispatcher.focus_visible?
|
|
17
|
+
self
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def for(owner, static = nil)
|
|
21
|
+
result = @flags[owner]&.dup || Set.new
|
|
22
|
+
result.merge(static) if static
|
|
23
|
+
result
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def flags(owner) = @flags[owner] ||= Set.new
|
|
29
|
+
end
|
|
30
|
+
end
|
data/lib/zaniah/layout/engine.rb
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
module Zaniah
|
|
4
4
|
module Layout
|
|
5
5
|
class Engine
|
|
6
|
+
include GridLayout
|
|
7
|
+
EDGE_KEYS = {
|
|
8
|
+
padding: [%i[padding_top padding_right padding_bottom padding_left], :padding_start, :padding_end],
|
|
9
|
+
margin: [%i[margin_top margin_right margin_bottom margin_left], :margin_start, :margin_end],
|
|
10
|
+
border: [%i[border_top border_right border_bottom border_left], :border_start, :border_end]
|
|
11
|
+
}.freeze
|
|
12
|
+
ZERO_EDGES = [0, 0, 0, 0].freeze
|
|
13
|
+
|
|
6
14
|
def initialize(rem: 16) = @rem = rem
|
|
7
15
|
|
|
8
16
|
def compute(root, width:, height:, x: 0, y: 0)
|
|
@@ -15,11 +23,23 @@ module Zaniah
|
|
|
15
23
|
|
|
16
24
|
def resolve_length(length, available, fallback = nil)
|
|
17
25
|
return fallback if length.nil? || length == :auto
|
|
18
|
-
|
|
26
|
+
return length.resolve(available || 0, rem: @rem) if length.is_a?(Length)
|
|
27
|
+
length.is_a?(Numeric) ? length : Float(length)
|
|
19
28
|
end
|
|
20
29
|
|
|
21
30
|
def resolve_edges(style, name, available)
|
|
22
|
-
|
|
31
|
+
values = style.to_h
|
|
32
|
+
input = values[name]
|
|
33
|
+
direction = values[:direction]
|
|
34
|
+
raise ArgumentError, "direction must be ltr or rtl" unless %i[ltr rtl].include?(direction)
|
|
35
|
+
physical, start_key, end_key = EDGE_KEYS.fetch(name)
|
|
36
|
+
sides = physical.map { |key| values[key] }
|
|
37
|
+
start, finish = values[start_key], values[end_key]
|
|
38
|
+
if input.is_a?(Numeric) && sides.none? && start.nil? && finish.nil?
|
|
39
|
+
return ZERO_EDGES if input.zero?
|
|
40
|
+
return [input, input, input, input]
|
|
41
|
+
end
|
|
42
|
+
|
|
23
43
|
input = input.to_h.values if input.is_a?(Edges)
|
|
24
44
|
input = [input] unless input.is_a?(Array)
|
|
25
45
|
input = case input.length
|
|
@@ -29,26 +49,59 @@ module Zaniah
|
|
|
29
49
|
when 4 then input
|
|
30
50
|
else raise ArgumentError, "invalid #{name}"
|
|
31
51
|
end
|
|
32
|
-
|
|
33
|
-
val =
|
|
52
|
+
values = sides.each_with_index.map do |value, i|
|
|
53
|
+
val = value || input[i]
|
|
34
54
|
val == :auto ? :auto : resolve_length(val, available, 0)
|
|
35
55
|
end
|
|
56
|
+
left, right = direction == :ltr ? [start, finish] : [finish, start]
|
|
57
|
+
values[3] = left == :auto ? :auto : resolve_length(left, available, 0) unless left.nil?
|
|
58
|
+
values[1] = right == :auto ? :auto : resolve_length(right, available, 0) unless right.nil?
|
|
59
|
+
values
|
|
36
60
|
end
|
|
37
61
|
|
|
38
62
|
def clamp_size(node, dimension, size, available)
|
|
39
|
-
|
|
40
|
-
|
|
63
|
+
clamp_value(node.style.to_h, dimension, size, available)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def clamp_value(values, dimension, size, available)
|
|
67
|
+
if dimension == :width
|
|
68
|
+
min, max = values[:min_width], values[:max_width]
|
|
69
|
+
else
|
|
70
|
+
min, max = values[:min_height], values[:max_height]
|
|
71
|
+
end
|
|
72
|
+
return size.clamp(min, [min, max].max) if min.is_a?(Numeric) && max.is_a?(Numeric)
|
|
73
|
+
min = resolve_length(min, available, 0)
|
|
74
|
+
max = resolve_length(max, available, Float::INFINITY)
|
|
41
75
|
size.clamp(min, [min, max].max)
|
|
42
76
|
end
|
|
43
77
|
|
|
44
78
|
def natural_size(node, available_w, available_h)
|
|
45
|
-
|
|
79
|
+
style = node.style
|
|
80
|
+
s = style.to_h
|
|
46
81
|
return [0, 0] if s[:display] == :none
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
82
|
+
if !node.measure && node.children.empty? && s.length == Style::DEFAULTS.length &&
|
|
83
|
+
s[:padding] == 0 && s[:border] == 0 && !s[:aspect_ratio] &&
|
|
84
|
+
!s[:padding_start] && !s[:padding_end] && !s[:border_start] && !s[:border_end] &&
|
|
85
|
+
s[:min_width] == 0 && s[:min_height] == 0 &&
|
|
86
|
+
s[:max_width] == Float::INFINITY && s[:max_height] == Float::INFINITY
|
|
87
|
+
return [resolve_length(s[:width], available_w, 0), resolve_length(s[:height], available_h, 0)]
|
|
88
|
+
end
|
|
89
|
+
padding, border = uniform_edge(style, :padding), uniform_edge(style, :border)
|
|
90
|
+
if padding && border
|
|
91
|
+
padding_w = padding_h = (padding + border) * 2
|
|
92
|
+
else
|
|
93
|
+
p = resolve_edges(style, :padding, available_w).map { |edge| edge == :auto ? 0 : edge }
|
|
94
|
+
b = resolve_edges(style, :border, available_w).map { |edge| edge == :auto ? 0 : edge }
|
|
95
|
+
padding_w, padding_h = p[1] + p[3] + b[1] + b[3], p[0] + p[2] + b[0] + b[2]
|
|
96
|
+
end
|
|
50
97
|
w = resolve_length(s[:width], available_w)
|
|
51
98
|
h = resolve_length(s[:height], available_h)
|
|
99
|
+
if s[:aspect_ratio]
|
|
100
|
+
ratio = Float(s[:aspect_ratio])
|
|
101
|
+
raise ArgumentError, "aspect ratio must be positive and finite" unless ratio.positive? && ratio.finite?
|
|
102
|
+
h = w / ratio if w && !h
|
|
103
|
+
w = h * ratio if h && !w
|
|
104
|
+
end
|
|
52
105
|
if node.measure
|
|
53
106
|
measured = node.measure.call([available_w - padding_w, 0].max, [available_h - padding_h, 0].max)
|
|
54
107
|
w ||= measured[0] + padding_w
|
|
@@ -61,10 +114,17 @@ module Zaniah
|
|
|
61
114
|
w ||= (row ? sizes.sum(&:first) + gap : sizes.map(&:first).max || 0) + padding_w
|
|
62
115
|
h ||= (row ? sizes.map(&:last).max || 0 : sizes.sum(&:last) + gap) + padding_h
|
|
63
116
|
end
|
|
64
|
-
[
|
|
117
|
+
[clamp_value(s, :width, w || padding_w, available_w), clamp_value(s, :height, h || padding_h, available_h)]
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def uniform_edge(style, name)
|
|
121
|
+
values = style.to_h
|
|
122
|
+
physical, start_key, end_key = EDGE_KEYS.fetch(name)
|
|
123
|
+
value = values[name]
|
|
124
|
+
value if value.is_a?(Numeric) && physical.none? { |key| values[key] } && values[start_key].nil? && values[end_key].nil?
|
|
65
125
|
end
|
|
66
126
|
|
|
67
|
-
def layout(node, x, y, width, height, available_w, available_h)
|
|
127
|
+
def layout(node, x, y, width, height, available_w, available_h, clamp = true)
|
|
68
128
|
key = [node.generation, x, y, width, height, available_w, available_h]
|
|
69
129
|
return if node.cache[key]
|
|
70
130
|
if node.style[:display] == :none
|
|
@@ -72,12 +132,20 @@ module Zaniah
|
|
|
72
132
|
node.children.each { |child| hide(child, x, y) }
|
|
73
133
|
return
|
|
74
134
|
end
|
|
75
|
-
|
|
76
|
-
|
|
135
|
+
if clamp
|
|
136
|
+
width = clamp_size(node, :width, width, available_w)
|
|
137
|
+
height = clamp_size(node, :height, height, available_h)
|
|
138
|
+
end
|
|
77
139
|
node.bounds = Bounds.new(x, y, width, height)
|
|
140
|
+
if node.children.empty?
|
|
141
|
+
node.cache.shift if node.cache.length >= 5
|
|
142
|
+
node.cache[key] = true
|
|
143
|
+
return
|
|
144
|
+
end
|
|
78
145
|
content = content_bounds(node)
|
|
79
146
|
flow, absolute = node.children.partition { |child| child.style[:position] != :absolute }
|
|
80
|
-
|
|
147
|
+
s = node.style
|
|
148
|
+
s[:display] == :grid ? layout_grid_children(node, content, flow) : layout_flow_children(node, content, flow)
|
|
81
149
|
layout_absolute_children(absolute, content)
|
|
82
150
|
node.cache.shift if node.cache.length >= 5
|
|
83
151
|
node.cache[key] = true
|
|
@@ -90,20 +158,27 @@ module Zaniah
|
|
|
90
158
|
end
|
|
91
159
|
|
|
92
160
|
def layout_flow_children(node, content, flow)
|
|
93
|
-
s = node.style
|
|
161
|
+
s = node.style.to_h
|
|
94
162
|
row = s[:flex_direction].to_s.start_with?("row")
|
|
95
163
|
reverse = s[:flex_direction].to_s.end_with?("reverse")
|
|
96
164
|
main_size, cross_size = row ? [content.width, content.height] : [content.height, content.width]
|
|
97
165
|
before, after, cross_before, cross_after = row ? [3, 1, 0, 2] : [0, 2, 3, 1]
|
|
98
166
|
main_dimension, cross_dimension = row ? [:width, :height] : [:height, :width]
|
|
99
167
|
gap = resolve_length(s[:gap], main_size, 0)
|
|
100
|
-
flow = flow.reject { |child| child.style[:display] == :none && hide(child, node.bounds.x, node.bounds.y) }
|
|
168
|
+
flow = flow.reject { |child| child.style.to_h[:display] == :none && hide(child, node.bounds.x, node.bounds.y) }
|
|
169
|
+
if s[:flex_wrap] == :nowrap && s[:justify_content] == :start && s[:align_items] == :stretch &&
|
|
170
|
+
layout_simple_flow(flow, content, row, reverse, main_size, cross_size, cross_dimension, gap)
|
|
171
|
+
return
|
|
172
|
+
end
|
|
101
173
|
items = flow.map do |child|
|
|
174
|
+
style = child.style.to_h
|
|
102
175
|
nw, nh = natural_size(child, content.width, content.height)
|
|
103
|
-
|
|
176
|
+
basis = style[:flex_basis]
|
|
177
|
+
base = resolve_length(basis, main_size, row ? nw : nh)
|
|
104
178
|
margins = resolve_edges(child.style, :margin, content.width)
|
|
105
|
-
|
|
106
|
-
|
|
179
|
+
size = basis == :auto ? base : clamp_size(child, main_dimension, base, main_size)
|
|
180
|
+
{node: child, base: base, size: size,
|
|
181
|
+
cross: row ? nh : nw, margins: margins, style: style}
|
|
107
182
|
end
|
|
108
183
|
lines = [[]]
|
|
109
184
|
occupied = 0
|
|
@@ -120,7 +195,10 @@ module Zaniah
|
|
|
120
195
|
cross_cursor = 0
|
|
121
196
|
lines.each do |line|
|
|
122
197
|
next if line.empty?
|
|
123
|
-
fixed_margin = line.sum
|
|
198
|
+
fixed_margin = line.sum do |item|
|
|
199
|
+
margins = item[:margins]
|
|
200
|
+
(margins[before] == :auto ? 0 : margins[before]) + (margins[after] == :auto ? 0 : margins[after])
|
|
201
|
+
end
|
|
124
202
|
distribute_space(line, main_size - gap * (line.length - 1) - fixed_margin, main_dimension)
|
|
125
203
|
free = main_size - line.sum { |i| i[:size] } - fixed_margin - gap * (line.length - 1)
|
|
126
204
|
autos = line.sum { |i| i[:margins].values_at(before, after).count(:auto) }
|
|
@@ -135,16 +213,20 @@ module Zaniah
|
|
|
135
213
|
else [0, gap]
|
|
136
214
|
end
|
|
137
215
|
line_cross = lines.length == 1 ? cross_size : line.map { |i| i[:cross] + i[:margins].values_at(cross_before, cross_after).sum { |v| v == :auto ? 0 : v } }.max
|
|
138
|
-
baseline = line.
|
|
216
|
+
baseline = line.reduce(0) { |value, item| item[:node].baseline > value ? item[:node].baseline : value }
|
|
139
217
|
cursor = leading
|
|
140
218
|
line.each do |item|
|
|
141
|
-
child, margins = item
|
|
142
|
-
mb
|
|
143
|
-
|
|
144
|
-
|
|
219
|
+
child, margins = item[:node], item[:margins]
|
|
220
|
+
mb = margins[before] == :auto ? auto_margin : margins[before]
|
|
221
|
+
ma = margins[after] == :auto ? auto_margin : margins[after]
|
|
222
|
+
cb = margins[cross_before] == :auto ? 0 : margins[cross_before]
|
|
223
|
+
ca = margins[cross_after] == :auto ? 0 : margins[cross_after]
|
|
224
|
+
style = item[:style]
|
|
225
|
+
align = style[:align_self] == :auto ? s[:align_items] : style[:align_self]
|
|
145
226
|
cross = item[:cross]
|
|
146
|
-
cross = [line_cross - cb - ca, 0].max if align == :stretch &&
|
|
147
|
-
|
|
227
|
+
cross = [line_cross - cb - ca, 0].max if align == :stretch && style[cross_dimension] == :auto
|
|
228
|
+
default_limits = cross_dimension == :width ? style[:min_width] == 0 && style[:max_width] == Float::INFINITY : style[:min_height] == 0 && style[:max_height] == Float::INFINITY
|
|
229
|
+
cross = clamp_size(child, cross_dimension, cross, cross_size) unless default_limits
|
|
148
230
|
cross_free = line_cross - cross - cb - ca
|
|
149
231
|
cross_offset = case align
|
|
150
232
|
when :end then cross_free
|
|
@@ -152,24 +234,79 @@ module Zaniah
|
|
|
152
234
|
when :baseline then baseline - child.baseline
|
|
153
235
|
else 0
|
|
154
236
|
end
|
|
155
|
-
cross_autos = margins
|
|
237
|
+
cross_autos = (margins[cross_before] == :auto ? 1 : 0) + (margins[cross_after] == :auto ? 1 : 0)
|
|
156
238
|
if cross_autos.positive?
|
|
157
239
|
cross_offset = margins[cross_before] == :auto ? [cross_free, 0].max / cross_autos : 0
|
|
158
240
|
end
|
|
159
241
|
cursor += mb
|
|
160
242
|
main_position = reverse ? main_size - cursor - item[:size] : cursor
|
|
161
243
|
cross_position = cross_cursor + cb + cross_offset
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
244
|
+
if row
|
|
245
|
+
cx, cy, cw, ch = content.x + main_position, content.y + cross_position, item[:size], cross
|
|
246
|
+
else
|
|
247
|
+
cx, cy, cw, ch = content.x + cross_position, content.y + main_position, cross, item[:size]
|
|
248
|
+
end
|
|
249
|
+
if style[:left] || style[:right]
|
|
250
|
+
cx += resolve_length(style[:left], content.width, 0) - resolve_length(style[:right], content.width, 0)
|
|
251
|
+
end
|
|
252
|
+
if style[:top] || style[:bottom]
|
|
253
|
+
cy += resolve_length(style[:top], content.height, 0) - resolve_length(style[:bottom], content.height, 0)
|
|
254
|
+
end
|
|
255
|
+
layout(child, cx, cy, cw, ch, content.width, content.height, false)
|
|
167
256
|
cursor += item[:size] + ma + spacing
|
|
168
257
|
end
|
|
169
258
|
cross_cursor += line_cross + gap
|
|
170
259
|
end
|
|
171
260
|
end
|
|
172
261
|
|
|
262
|
+
def layout_simple_flow(flow, content, row, reverse, main_size, cross_size, cross_dimension, gap)
|
|
263
|
+
styles = flow.map { |child| child.style.to_h }
|
|
264
|
+
return false unless styles.all? do |style|
|
|
265
|
+
margin = style[:margin]
|
|
266
|
+
margin.is_a?(Numeric) && margin.zero? && style[:position] == :relative && style[:flex_basis] == :auto &&
|
|
267
|
+
(style[:align_self] == :auto || style[:align_self] == :stretch) && style[:min_width] == 0 && style[:min_height] == 0 &&
|
|
268
|
+
style[:max_width] == Float::INFINITY && style[:max_height] == Float::INFINITY &&
|
|
269
|
+
style[:margin_top].nil? && style[:margin_right].nil? && style[:margin_bottom].nil? && style[:margin_left].nil? &&
|
|
270
|
+
style[:margin_start].nil? && style[:margin_end].nil? && style[:left].nil? && style[:right].nil? &&
|
|
271
|
+
style[:top].nil? && style[:bottom].nil? && style[:flex_grow].is_a?(Numeric) && style[:flex_shrink].is_a?(Numeric)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
bases, crosses = flow.map do |child|
|
|
275
|
+
width, height = natural_size(child, content.width, content.height)
|
|
276
|
+
row ? [width, height] : [height, width]
|
|
277
|
+
end.transpose
|
|
278
|
+
bases ||= []
|
|
279
|
+
crosses ||= []
|
|
280
|
+
available = main_size - gap * [flow.length - 1, 0].max
|
|
281
|
+
total = bases.sum
|
|
282
|
+
growing = total < available
|
|
283
|
+
factor = styles.each_with_index.sum do |style, index|
|
|
284
|
+
growing ? style[:flex_grow] : style[:flex_shrink] * bases[index]
|
|
285
|
+
end
|
|
286
|
+
free = available - total
|
|
287
|
+
divisor = growing ? [factor, 1].max : factor
|
|
288
|
+
sizes = bases.each_index.map do |index|
|
|
289
|
+
weight = growing ? styles[index][:flex_grow] : styles[index][:flex_shrink] * bases[index]
|
|
290
|
+
bases[index] + (factor.positive? ? free * weight / divisor : 0)
|
|
291
|
+
end
|
|
292
|
+
return false if sizes.any?(&:negative?)
|
|
293
|
+
|
|
294
|
+
cursor = 0
|
|
295
|
+
flow.each_with_index do |child, index|
|
|
296
|
+
style = styles[index]
|
|
297
|
+
size = sizes[index]
|
|
298
|
+
cross = style[cross_dimension] == :auto ? cross_size : crosses[index]
|
|
299
|
+
main_position = reverse ? main_size - cursor - size : cursor
|
|
300
|
+
if row
|
|
301
|
+
layout(child, content.x + main_position, content.y, size, cross, content.width, content.height, false)
|
|
302
|
+
else
|
|
303
|
+
layout(child, content.x, content.y + main_position, cross, size, content.width, content.height, false)
|
|
304
|
+
end
|
|
305
|
+
cursor += size + gap
|
|
306
|
+
end
|
|
307
|
+
true
|
|
308
|
+
end
|
|
309
|
+
|
|
173
310
|
def layout_absolute_children(children, content)
|
|
174
311
|
children.each do |child|
|
|
175
312
|
nw, nh = natural_size(child, content.width, content.height)
|
|
@@ -195,11 +332,11 @@ module Zaniah
|
|
|
195
332
|
active = line.dup
|
|
196
333
|
remaining = available
|
|
197
334
|
until active.empty?
|
|
198
|
-
factor = active.sum { |i| growing ? i[:
|
|
335
|
+
factor = active.sum { |i| growing ? i[:style][:flex_grow] : i[:style][:flex_shrink] * i[:base] }
|
|
199
336
|
free = remaining - active.sum { |i| i[:base] }
|
|
200
337
|
frozen = []
|
|
201
338
|
active.each do |item|
|
|
202
|
-
weight = growing ? item[:
|
|
339
|
+
weight = growing ? item[:style][:flex_grow] : item[:style][:flex_shrink] * item[:base]
|
|
203
340
|
proposed = item[:base] + (factor.positive? ? free * weight / [factor, growing ? 1 : factor].max : 0)
|
|
204
341
|
item[:size] = clamp_size(item[:node], dimension, proposed, available)
|
|
205
342
|
frozen << item if (item[:size] - proposed).abs > 0.00001 || weight.zero?
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Zaniah
|
|
4
|
+
module Layout
|
|
5
|
+
module GridLayout
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def layout_grid_children(node, content, children)
|
|
9
|
+
children = children.reject { |child| child.style[:display] == :none && hide(child, node.bounds.x, node.bounds.y) }
|
|
10
|
+
columns = Array(node.style[:grid_template_columns]).flatten
|
|
11
|
+
columns = [:auto] if columns.empty?
|
|
12
|
+
parsed = children.map do |child|
|
|
13
|
+
column, column_span = grid_placement(child.style[:grid_column])
|
|
14
|
+
row, row_span = grid_placement(child.style[:grid_row])
|
|
15
|
+
{node: child, column: column, column_span: column_span || 1, row: row, row_span: row_span || 1}
|
|
16
|
+
end
|
|
17
|
+
column_count = [columns.length, parsed.filter_map { |item| item[:column] && item[:column] + item[:column_span] }.max || 1].max
|
|
18
|
+
columns += Array.new(column_count - columns.length, :auto)
|
|
19
|
+
items = place_grid_items(parsed, column_count)
|
|
20
|
+
row_count = [Array(node.style[:grid_template_rows]).flatten.length,
|
|
21
|
+
items.map { |item| item[:row] + item[:row_span] }.max || 1].max
|
|
22
|
+
rows = Array(node.style[:grid_template_rows]).flatten
|
|
23
|
+
rows += Array.new(row_count - rows.length, :auto)
|
|
24
|
+
column_gap = resolve_length(node.style[:column_gap] || node.style[:gap], content.width, 0)
|
|
25
|
+
row_gap = resolve_length(node.style[:row_gap] || node.style[:gap], content.height, 0)
|
|
26
|
+
natural = items.to_h { |item| [item, natural_size(item[:node], content.width, content.height)] }
|
|
27
|
+
widths = grid_track_sizes(columns, content.width, column_gap,
|
|
28
|
+
grid_intrinsic(items, natural, :column, :column_span, 0, column_count))
|
|
29
|
+
heights = grid_track_sizes(rows, content.height, row_gap,
|
|
30
|
+
grid_intrinsic(items, natural, :row, :row_span, 1, row_count))
|
|
31
|
+
|
|
32
|
+
items.each do |item|
|
|
33
|
+
column, row = item.values_at(:column, :row)
|
|
34
|
+
width = widths.slice(column, item[:column_span]).sum + column_gap * (item[:column_span] - 1)
|
|
35
|
+
height = heights.slice(row, item[:row_span]).sum + row_gap * (item[:row_span] - 1)
|
|
36
|
+
x = content.x + widths.take(column).sum + column_gap * column
|
|
37
|
+
y = content.y + heights.take(row).sum + row_gap * row
|
|
38
|
+
layout(item[:node], x, y, width, height, content.width, content.height)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def grid_placement(value)
|
|
43
|
+
case value
|
|
44
|
+
when nil then [nil, nil]
|
|
45
|
+
when Integer
|
|
46
|
+
raise ArgumentError, "grid positions start at 1" unless value.positive?
|
|
47
|
+
[value - 1, 1]
|
|
48
|
+
when Range
|
|
49
|
+
first, last = Integer(value.begin), Integer(value.end)
|
|
50
|
+
last -= 1 if value.exclude_end?
|
|
51
|
+
raise ArgumentError, "invalid grid range" unless first.positive? && last >= first
|
|
52
|
+
[first - 1, last - first + 1]
|
|
53
|
+
else raise ArgumentError, "grid placement must be an Integer or Range"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def place_grid_items(items, columns)
|
|
58
|
+
occupied = {}
|
|
59
|
+
cursor = [0, 0]
|
|
60
|
+
items.each_with_index.sort_by { |item, index| [item[:row] && item[:column] ? 0 : item[:row] || item[:column] ? 1 : 2, index] }.each do |item, _index|
|
|
61
|
+
explicit = item[:row] || item[:column]
|
|
62
|
+
row, column = next_grid_cell(occupied, columns, item, cursor)
|
|
63
|
+
item[:row], item[:column] = row, column
|
|
64
|
+
item[:row_span].times { |dy| item[:column_span].times { |dx| occupied[[row + dy, column + dx]] = true } }
|
|
65
|
+
cursor = column + item[:column_span] >= columns ? [row + 1, 0] : [row, column + item[:column_span]] unless explicit
|
|
66
|
+
end
|
|
67
|
+
items
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def next_grid_cell(occupied, columns, item, cursor)
|
|
71
|
+
column_span, row_span = item.values_at(:column_span, :row_span)
|
|
72
|
+
raise ArgumentError, "grid item span exceeds column count" if column_span > columns
|
|
73
|
+
fixed_row, fixed_column = item.values_at(:row, :column)
|
|
74
|
+
row = fixed_row || (fixed_column ? 0 : cursor[0])
|
|
75
|
+
column = fixed_column || (fixed_row ? 0 : cursor[1])
|
|
76
|
+
loop do
|
|
77
|
+
fits = column + column_span <= columns && row_span.times.all? do |dy|
|
|
78
|
+
column_span.times.none? { |dx| occupied[[row + dy, column + dx]] }
|
|
79
|
+
end
|
|
80
|
+
return [row, column] if fits
|
|
81
|
+
raise ArgumentError, "grid row is already occupied" if fixed_row && fixed_column
|
|
82
|
+
if fixed_column
|
|
83
|
+
row += 1
|
|
84
|
+
else
|
|
85
|
+
column += 1
|
|
86
|
+
row, column = [row + 1, 0] if column + column_span > columns
|
|
87
|
+
raise ArgumentError, "grid row is already occupied" if fixed_row && row > fixed_row
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def grid_intrinsic(items, natural, start_key, span_key, dimension, count)
|
|
93
|
+
values = Array.new(count, 0.0)
|
|
94
|
+
items.each do |item|
|
|
95
|
+
start, span = item.values_at(start_key, span_key)
|
|
96
|
+
wanted = natural.fetch(item)[dimension]
|
|
97
|
+
share = ([wanted - values.slice(start, span).sum, 0].max / span)
|
|
98
|
+
span.times { |index| values[start + index] += share }
|
|
99
|
+
end
|
|
100
|
+
values
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def grid_track_sizes(specs, available, gap, intrinsic)
|
|
104
|
+
total = [available - gap * [specs.length - 1, 0].max, 0].max
|
|
105
|
+
sizes = specs.each_with_index.map { |spec, index| grid_track_min(spec, total, intrinsic[index]) }
|
|
106
|
+
weights = specs.map { |spec| grid_fraction(spec) }
|
|
107
|
+
active = weights.each_index.select { |index| weights[index].positive? }
|
|
108
|
+
remaining = total - sizes.each_index.reject { |index| active.include?(index) }.sum { |index| sizes[index] }
|
|
109
|
+
until active.empty?
|
|
110
|
+
unit = remaining / active.sum { |index| weights[index] }
|
|
111
|
+
constrained = active.select { |index| unit * weights[index] < sizes[index] }
|
|
112
|
+
if constrained.empty?
|
|
113
|
+
active.each { |index| sizes[index] = unit * weights[index] }
|
|
114
|
+
break
|
|
115
|
+
end
|
|
116
|
+
constrained.each { |index| remaining -= sizes[index]; active.delete(index) }
|
|
117
|
+
end
|
|
118
|
+
sizes
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def grid_track_min(spec, available, intrinsic)
|
|
122
|
+
spec = spec.min if spec.is_a?(MinMax)
|
|
123
|
+
return intrinsic if spec.nil? || spec == :auto
|
|
124
|
+
return 0.0 if spec.is_a?(Length) && spec.unit == :fr
|
|
125
|
+
resolve_length(spec, available, 0)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def grid_fraction(spec)
|
|
129
|
+
spec = spec.max if spec.is_a?(MinMax)
|
|
130
|
+
spec.is_a?(Length) && spec.unit == :fr ? spec.value.to_f : 0.0
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|