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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 63a0e6137fddcdcd90ebeb7f64a4a5d8b7d24bce082a87f27273eadc3d20a3e7
4
- data.tar.gz: 925a673a508c582bb259da12794653b65ac30d6184e9b48f586db0ae3d0de7ba
3
+ metadata.gz: b3edeedc6b7d09bb964b5f3e6ba7306ced50355bfffb93c049c9cc7415cab4d2
4
+ data.tar.gz: 3944a4cef79f57035ac08d5dbeba1fef7e96504c5ea34c3237e2dad08dcdde31
5
5
  SHA512:
6
- metadata.gz: 18cc22164930e56efe5c768fd947c7dd23009b49d2266b5d849b71dedae30202a4b32540a9c595ec5490ae4b5ee7a6f0bf167796fcbd5fb41e674db312696840
7
- data.tar.gz: 4b2ed08745f24cc99082d604247e1cf1e501f2188f217ed70ced49065c22587e59c335ce11aede01a47ba2626de78b9cccb2fafa50abea8273b86eda779f0736
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
- | Guide | Purpose |
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 forwarded to Pane.
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(name: name, content: content, block: block, view: view, **options)
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 focusable
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
- # The pane's focus slot name, fixed width, fixed height, and grow weight.
11
- attr_reader :name, :width, :height, :grow
12
-
13
- # *name* is the focus slot identifier (optional). *content* or *block* provides the body.
14
- # *width*/*height*/*grow* control sizing. *border* may be `true` (normal border) or a
15
- # border name symbol. *padding* may be 1, 2, or 4 values (CSS-style shorthand).
16
- # *style* sets the base style; *focused_style* overrides it when the pane is focused.
17
- # *focus: true* marks the pane as focusable. *scroll*/*clip*/*wrap* control how
18
- # overflow content is rendered (via the embedded Viewport).
19
- def initialize(name: nil, content: nil, block: nil, view: nil, width: nil, height: nil, grow: nil, border: nil, padding: nil, style: nil, focused_style: nil, focus: false, scroll: false, clip: true, wrap: false)
20
- @name = name
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 marked focusable and has a name, otherwise [].
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: inner_rect(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
- outer_style(rect).render(rendered_content(rect))
43
+ inner = @geometry.inset(rect)
44
+ outer_style(inner).render(rendered_content(inner))
58
45
  end
59
46
 
60
47
  private
61
48
 
62
- # The raw content, the body block, the view used for instance_exec, and styling options.
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
- # Evaluates the configured content (block or constant) and renders it to a string.
75
- # Pane blocks may accept the inner content Rect when they need exact available dimensions.
76
- def evaluate_content(content_rect)
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) ? value.render.to_s : value.to_s
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(rect)
88
- styled = current_style
89
- styled = styled.border(border_style) if border
90
- styled = styled.padding(*padding_values) if padding
91
- styled.width(inner_rect(rect).width).height(inner_rect(rect).height)
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
- # Returns the active style: the focused variant when the pane is focused, otherwise
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Charming
4
- VERSION = "0.1.4"
4
+ VERSION = "0.2.0"
5
5
  end
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.1.4
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