charming 0.1.4 → 0.2.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/README.md +1 -14
- data/lib/charming/presentation/layout/builder.rb +8 -2
- data/lib/charming/presentation/layout/pane.rb +34 -109
- data/lib/charming/presentation/layout/pane_behavior.rb +39 -0
- data/lib/charming/presentation/layout/pane_geometry.rb +71 -0
- data/lib/charming/presentation/layout/pane_style.rb +39 -0
- data/lib/charming/version.rb +1 -1
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b3edeedc6b7d09bb964b5f3e6ba7306ced50355bfffb93c049c9cc7415cab4d2
|
|
4
|
+
data.tar.gz: 3944a4cef79f57035ac08d5dbeba1fef7e96504c5ea34c3237e2dad08dcdde31
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ea1881f3bf0d3517cc2a0f20b8ae15fcd452c091d71f6535bc632a7dd66be6abaf97fa3ca76aef567be068be41a374c11e263529cc43dad931ac7fe344847061
|
|
7
|
+
data.tar.gz: 6b396f225bdb25bf3cd1f5ff569d4907a9e2e0be8bbfc4ad500021251f1bc39ba65b270b80d28b7dd9fc087294fc9a91d4fb4da5dfbdfbdced19040074093c0a
|
data/README.md
CHANGED
|
@@ -29,20 +29,7 @@ Charming can also be added to an existing Ruby project with Bundler, but the pri
|
|
|
29
29
|
|
|
30
30
|
## Documentation
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|-------|---------|
|
|
34
|
-
| [Docs Index](docs/README.md) | Suggested reading paths and all documentation links. |
|
|
35
|
-
| [Getting Started](docs/getting_started.md) | Build and run a generated Charming app. |
|
|
36
|
-
| [Core Concepts](docs/core_concepts.md) | App architecture, runtime flow, ephemeral controllers, and state. |
|
|
37
|
-
| [Routing](docs/routing.md) | `root`, `screen`, dynamic params, route titles, and route order. |
|
|
38
|
-
| [Controllers & Templates](docs/controllers_and_templates.md) | Actions, `render :show`, `render_template`, key bindings, commands, timers, and tasks. |
|
|
39
|
-
| [Layouts](docs/layouts.md) | Template layouts, `yield_content`, split panes, overlays, responsive layouts, and styles. |
|
|
40
|
-
| [State](docs/state.md) | `ApplicationState`, typed attributes, validations, and session-backed state. |
|
|
41
|
-
| [Database](docs/database.md) | Optional SQLite persistence with Active Record models and migrations. |
|
|
42
|
-
| [Components](docs/components.md) | Built-in components, custom components, and interaction return values. |
|
|
43
|
-
| [Themes](docs/themes.md) | Theme registration, tokens, and runtime theme switching. |
|
|
44
|
-
| [API Reference](docs/api.md) | Compact public API reference. |
|
|
45
|
-
| [Testing](docs/testing.md) | Controller, template, component, runtime, timer, and task tests. |
|
|
32
|
+
- [Docs](https://charming.sh)
|
|
46
33
|
|
|
47
34
|
## Generated App Structure
|
|
48
35
|
|
|
@@ -36,9 +36,15 @@ module Charming
|
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
# Adds a Pane leaf node to the current scope. *name* (optional) is the focus slot name;
|
|
39
|
-
# *content* (or a *block*) is the body. *options* are
|
|
39
|
+
# *content* (or a *block*) is the body. *options* are split into PaneGeometry,
|
|
40
|
+
# PaneStyle, and PaneBehavior keyword args.
|
|
40
41
|
def pane(name = nil, content = nil, **options, &block)
|
|
41
|
-
node = Pane.new(
|
|
42
|
+
node = Pane.new(
|
|
43
|
+
name: name, content: content, block: block, view: view,
|
|
44
|
+
geometry: PaneGeometry.build(**options.slice(:width, :height, :grow, :border, :padding)),
|
|
45
|
+
style: PaneStyle.build(**options.slice(:style, :focused_style)),
|
|
46
|
+
behavior: PaneBehavior.build(**options.slice(:focus, :scroll, :clip, :wrap))
|
|
47
|
+
)
|
|
42
48
|
append(node)
|
|
43
49
|
node
|
|
44
50
|
end
|
|
@@ -4,34 +4,20 @@ module Charming
|
|
|
4
4
|
module Layout
|
|
5
5
|
# Pane is a leaf layout node: a single rectangle with optional border, padding, and
|
|
6
6
|
# styling, containing a piece of content (a string, a View, or a block evaluated in the
|
|
7
|
-
# view's context). Panes with a `name` and `focus: true` are registered as
|
|
8
|
-
# slots in the controller's focus ring.
|
|
7
|
+
# view's context). Panes with a `name` and `behavior.focus: true` are registered as
|
|
8
|
+
# focusable slots in the controller's focus ring.
|
|
9
9
|
class Pane
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
# *name* is the focus slot identifier
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
@
|
|
21
|
-
@content = content
|
|
22
|
-
@block = block
|
|
23
|
-
@view = view
|
|
24
|
-
@width = width
|
|
25
|
-
@height = height
|
|
26
|
-
@grow = grow
|
|
27
|
-
@border = border
|
|
28
|
-
@padding = padding
|
|
29
|
-
@style = style
|
|
30
|
-
@focused_style = focused_style
|
|
31
|
-
@focus = focus
|
|
32
|
-
@scroll = scroll
|
|
33
|
-
@clip = clip
|
|
34
|
-
@wrap = wrap
|
|
10
|
+
attr_reader :name
|
|
11
|
+
delegate :width, :height, :grow, to: :geometry
|
|
12
|
+
|
|
13
|
+
# *name* is the focus slot identifier. *content* (or a *block*) is the body; *view*
|
|
14
|
+
# is the view used for instance_exec when the block is given. *geometry*, *style*, and
|
|
15
|
+
# *behavior* are value objects that own sizing, styling, and render-time flags.
|
|
16
|
+
def initialize(name: nil, content: nil, block: nil, view: nil,
|
|
17
|
+
geometry: PaneGeometry.new, style: PaneStyle.new,
|
|
18
|
+
behavior: PaneBehavior.new)
|
|
19
|
+
@name, @content, @block, @view = name, content, block, view
|
|
20
|
+
@geometry, @style, @behavior = geometry, style, behavior
|
|
35
21
|
end
|
|
36
22
|
|
|
37
23
|
# Raises ArgumentError — panes are leaves and cannot contain layout children.
|
|
@@ -39,117 +25,56 @@ module Charming
|
|
|
39
25
|
raise ArgumentError, "pane cannot contain layout children"
|
|
40
26
|
end
|
|
41
27
|
|
|
42
|
-
# Returns [name] when the pane is
|
|
28
|
+
# Returns [name] when the pane is focusable and named, otherwise [].
|
|
43
29
|
def focusable_names
|
|
44
|
-
(focus && name) ? [name] : []
|
|
30
|
+
(@behavior.focus && name) ? [name] : []
|
|
45
31
|
end
|
|
46
32
|
|
|
47
33
|
# Returns the mouse target represented by this pane, if it has a name.
|
|
48
34
|
def mouse_targets(rect)
|
|
49
35
|
return [] unless name
|
|
50
36
|
|
|
51
|
-
[{name: name, rect: rect, inner_rect:
|
|
37
|
+
[{name: name, rect: rect, inner_rect: @geometry.inset(rect)}]
|
|
52
38
|
end
|
|
53
39
|
|
|
54
40
|
# Renders the pane into *rect*, applying the configured style, border, and padding
|
|
55
41
|
# around the evaluated content.
|
|
56
42
|
def render(rect)
|
|
57
|
-
|
|
43
|
+
inner = @geometry.inset(rect)
|
|
44
|
+
outer_style(inner).render(rendered_content(inner))
|
|
58
45
|
end
|
|
59
46
|
|
|
60
47
|
private
|
|
61
48
|
|
|
62
|
-
|
|
63
|
-
attr_reader :content, :block, :view, :border, :padding, :style, :focused_style, :focus, :scroll, :clip, :wrap
|
|
64
|
-
|
|
65
|
-
# Returns the content string for *rect*, optionally clipped/scrolled by an embedded Viewport.
|
|
66
|
-
def rendered_content(rect)
|
|
67
|
-
content_rect = inner_rect(rect)
|
|
68
|
-
value = evaluate_content(content_rect)
|
|
69
|
-
return value unless clip || scroll
|
|
70
|
-
|
|
71
|
-
Components::Viewport.new(content: value, width: content_rect.width, height: content_rect.height, wrap: wrap).render
|
|
72
|
-
end
|
|
49
|
+
attr_reader :content, :block, :view, :geometry, :style, :behavior
|
|
73
50
|
|
|
74
|
-
#
|
|
75
|
-
#
|
|
76
|
-
def
|
|
51
|
+
# Returns the content string for *content_rect*, optionally clipped/scrolled by an
|
|
52
|
+
# embedded Viewport when *behavior.should_viewport?* is true.
|
|
53
|
+
def rendered_content(content_rect)
|
|
77
54
|
value = if block
|
|
78
55
|
block.arity.zero? ? view.instance_exec(&block) : view.instance_exec(content_rect, &block)
|
|
79
56
|
else
|
|
80
57
|
content
|
|
81
58
|
end
|
|
82
|
-
value.respond_to?(:render)
|
|
59
|
+
return value.to_s unless value.respond_to?(:render)
|
|
60
|
+
return value.render.to_s unless @behavior.should_viewport?
|
|
61
|
+
|
|
62
|
+
Components::Viewport.new(content: value.render, width: content_rect.width,
|
|
63
|
+
height: content_rect.height, wrap: @behavior.wrap).render.to_s
|
|
83
64
|
end
|
|
84
65
|
|
|
85
66
|
# Builds the outer style object with optional border and padding, sized to the
|
|
86
67
|
# inner rect of the pane.
|
|
87
|
-
def outer_style(
|
|
88
|
-
styled =
|
|
89
|
-
styled = styled.border(border_style) if border
|
|
90
|
-
styled = styled.padding(
|
|
91
|
-
styled.width(
|
|
68
|
+
def outer_style(inner)
|
|
69
|
+
styled = @style.resolve(view, focused: focused?)
|
|
70
|
+
styled = styled.border(@geometry.border_style) if @geometry.border
|
|
71
|
+
styled = styled.padding(*@geometry.padding_values) if @geometry.padding
|
|
72
|
+
styled.width(inner.width).height(inner.height)
|
|
92
73
|
end
|
|
93
74
|
|
|
94
|
-
#
|
|
95
|
-
# the configured style or a default UI::Style.
|
|
96
|
-
def current_style
|
|
97
|
-
return focused_pane_style if focused?
|
|
98
|
-
|
|
99
|
-
style || UI.style
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
# Returns the focused-pane style: the focused_style override, or the theme's title style.
|
|
103
|
-
def focused_pane_style
|
|
104
|
-
focused_style || view.__send__(:theme).title
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
# True when the pane is configured for focus and the view reports it as currently focused.
|
|
75
|
+
# True when the pane is configured for focus and the view reports it as focused.
|
|
108
76
|
def focused?
|
|
109
|
-
focus && name && view.focused?(name)
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
# Returns the inner Rect after border and padding insets are applied.
|
|
113
|
-
def inner_rect(rect)
|
|
114
|
-
rect.inset(
|
|
115
|
-
top: border_top + padding_top,
|
|
116
|
-
right: border_right + padding_right,
|
|
117
|
-
bottom: border_bottom + padding_bottom,
|
|
118
|
-
left: border_left + padding_left
|
|
119
|
-
)
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
# Resolves the border style symbol: :normal when border is `true`, otherwise the configured value.
|
|
123
|
-
def border_style
|
|
124
|
-
(border == true) ? :normal : border
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
# Border thickness on each side (1 when a border is configured, 0 otherwise).
|
|
128
|
-
def border_top = border ? 1 : 0
|
|
129
|
-
def border_right = border ? 1 : 0
|
|
130
|
-
def border_bottom = border ? 1 : 0
|
|
131
|
-
def border_left = border ? 1 : 0
|
|
132
|
-
|
|
133
|
-
# The padding values normalized to [top, right, bottom, left] form.
|
|
134
|
-
def padding_values
|
|
135
|
-
@padding_values ||= expand_padding(Array(padding))
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
# Per-side padding values (0 when no padding is configured).
|
|
139
|
-
def padding_top = padding ? padding_values[0] : 0
|
|
140
|
-
def padding_right = padding ? padding_values[1] : 0
|
|
141
|
-
def padding_bottom = padding ? padding_values[2] : 0
|
|
142
|
-
def padding_left = padding ? padding_values[3] : 0
|
|
143
|
-
|
|
144
|
-
# Normalizes 1/2/4 padding arguments to [top, right, bottom, left].
|
|
145
|
-
def expand_padding(values)
|
|
146
|
-
case values.length
|
|
147
|
-
when 1 then [values[0], values[0], values[0], values[0]]
|
|
148
|
-
when 2 then [values[0], values[1], values[0], values[1]]
|
|
149
|
-
when 4 then values
|
|
150
|
-
else
|
|
151
|
-
raise ArgumentError, "padding expects 1, 2, or 4 values"
|
|
152
|
-
end
|
|
77
|
+
@behavior.focus && name && view.focused?(name)
|
|
153
78
|
end
|
|
154
79
|
end
|
|
155
80
|
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Charming
|
|
4
|
+
module Layout
|
|
5
|
+
# PaneBehavior holds the render-time options that control a Pane's
|
|
6
|
+
# interaction with the focus ring and the embedded Viewport.
|
|
7
|
+
class PaneBehavior
|
|
8
|
+
attr_reader :focus, :scroll, :clip, :wrap
|
|
9
|
+
|
|
10
|
+
def self.build(focus: false, scroll: false, clip: true, wrap: false)
|
|
11
|
+
new(focus: focus, scroll: scroll, clip: clip, wrap: wrap)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def initialize(focus:, scroll:, clip:, wrap:)
|
|
15
|
+
@focus, @scroll, @clip, @wrap = focus, scroll, clip, wrap
|
|
16
|
+
freeze
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def ==(other)
|
|
20
|
+
other.is_a?(PaneBehavior) &&
|
|
21
|
+
focus == other.focus && scroll == other.scroll &&
|
|
22
|
+
clip == other.clip && wrap == other.wrap
|
|
23
|
+
end
|
|
24
|
+
alias_method :eql?, :==
|
|
25
|
+
|
|
26
|
+
def hash
|
|
27
|
+
[focus, scroll, clip, wrap].hash
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def focusable?
|
|
31
|
+
focus
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def should_viewport?
|
|
35
|
+
clip || scroll
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Charming
|
|
4
|
+
module Layout
|
|
5
|
+
# PaneGeometry holds a Pane's sizing (width, height, grow) and inset
|
|
6
|
+
# configuration (border + padding). It knows how to inset a Rect for the
|
|
7
|
+
# content area and how to expand CSS-style 1/2/4-value padding.
|
|
8
|
+
class PaneGeometry
|
|
9
|
+
attr_reader :width, :height, :grow, :border, :padding
|
|
10
|
+
|
|
11
|
+
def self.build(width: nil, height: nil, grow: nil, border: nil, padding: nil)
|
|
12
|
+
new(width: width, height: height, grow: grow,
|
|
13
|
+
border: (border == true) ? :normal : border, padding: padding)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def initialize(width:, height:, grow:, border:, padding:)
|
|
17
|
+
@width, @height, @grow, @border, @padding = width, height, grow, border, padding
|
|
18
|
+
@padding_values = padding ? expand_padding(Array(padding)) : [0, 0, 0, 0]
|
|
19
|
+
freeze
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def ==(other)
|
|
23
|
+
other.is_a?(PaneGeometry) &&
|
|
24
|
+
width == other.width && height == other.height && grow == other.grow &&
|
|
25
|
+
border == other.border && padding == other.padding
|
|
26
|
+
end
|
|
27
|
+
alias_method :eql?, :==
|
|
28
|
+
|
|
29
|
+
def hash
|
|
30
|
+
[width, height, grow, border, padding].hash
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def border_top = border ? 1 : 0
|
|
34
|
+
def border_right = border ? 1 : 0
|
|
35
|
+
def border_bottom = border ? 1 : 0
|
|
36
|
+
def border_left = border ? 1 : 0
|
|
37
|
+
|
|
38
|
+
attr_reader :padding_values
|
|
39
|
+
|
|
40
|
+
def padding_top = padding ? @padding_values[0] : 0
|
|
41
|
+
def padding_right = padding ? @padding_values[1] : 0
|
|
42
|
+
def padding_bottom = padding ? @padding_values[2] : 0
|
|
43
|
+
def padding_left = padding ? @padding_values[3] : 0
|
|
44
|
+
|
|
45
|
+
def inset(rect)
|
|
46
|
+
rect.inset(
|
|
47
|
+
top: border_top + padding_top,
|
|
48
|
+
right: border_right + padding_right,
|
|
49
|
+
bottom: border_bottom + padding_bottom,
|
|
50
|
+
left: border_left + padding_left
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def border_style
|
|
55
|
+
(border == true) ? :normal : border
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def expand_padding(values)
|
|
61
|
+
case values.length
|
|
62
|
+
when 1 then [values[0], values[0], values[0], values[0]]
|
|
63
|
+
when 2 then [values[0], values[1], values[0], values[1]]
|
|
64
|
+
when 4 then values
|
|
65
|
+
else
|
|
66
|
+
raise ArgumentError, "padding expects 1, 2, or 4 values"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Charming
|
|
4
|
+
module Layout
|
|
5
|
+
# PaneStyle holds a Pane's base style and the focused-state override.
|
|
6
|
+
# It resolves which style to use at render time given the pane's current
|
|
7
|
+
# focus state and the view's theme.
|
|
8
|
+
class PaneStyle
|
|
9
|
+
attr_reader :style, :focused_style
|
|
10
|
+
|
|
11
|
+
def self.build(style: nil, focused_style: nil)
|
|
12
|
+
new(style: style, focused_style: focused_style)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize(style:, focused_style:)
|
|
16
|
+
@style, @focused_style = style, focused_style
|
|
17
|
+
freeze
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def ==(other)
|
|
21
|
+
other.is_a?(PaneStyle) &&
|
|
22
|
+
style == other.style && focused_style == other.focused_style
|
|
23
|
+
end
|
|
24
|
+
alias_method :eql?, :==
|
|
25
|
+
|
|
26
|
+
def hash
|
|
27
|
+
[style, focused_style].hash
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Returns the active style for *focused*: the focused override when the
|
|
31
|
+
# pane is focused, otherwise the configured *style* or a default UI::Style.
|
|
32
|
+
def resolve(view, focused:)
|
|
33
|
+
return focused_style || view.__send__(:theme).title if focused
|
|
34
|
+
|
|
35
|
+
style || UI.style
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
data/lib/charming/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: charming
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- pando
|
|
@@ -335,6 +335,9 @@ files:
|
|
|
335
335
|
- lib/charming/presentation/layout/builder.rb
|
|
336
336
|
- lib/charming/presentation/layout/overlay.rb
|
|
337
337
|
- lib/charming/presentation/layout/pane.rb
|
|
338
|
+
- lib/charming/presentation/layout/pane_behavior.rb
|
|
339
|
+
- lib/charming/presentation/layout/pane_geometry.rb
|
|
340
|
+
- lib/charming/presentation/layout/pane_style.rb
|
|
338
341
|
- lib/charming/presentation/layout/rect.rb
|
|
339
342
|
- lib/charming/presentation/layout/screen_layout.rb
|
|
340
343
|
- lib/charming/presentation/layout/split.rb
|