charming 0.1.2 → 0.1.3

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.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/lib/charming/application.rb +3 -3
  3. data/lib/charming/controller/class_methods.rb +2 -2
  4. data/lib/charming/controller/command_palette.rb +2 -2
  5. data/lib/charming/controller/rendering.rb +2 -2
  6. data/lib/charming/controller/session_state.rb +1 -1
  7. data/lib/charming/generators/component_generator.rb +1 -1
  8. data/lib/charming/generators/templates/app/application.template +1 -1
  9. data/lib/charming/generators/templates/app/layout.template +3 -6
  10. data/lib/charming/generators/templates/app/view.template +1 -1
  11. data/lib/charming/generators/templates/component/component.rb.template +1 -1
  12. data/lib/charming/generators/templates/screen/view.rb.template +1 -1
  13. data/lib/charming/generators/templates/view/view.rb.template +1 -1
  14. data/lib/charming/internal/renderer/differential.rb +13 -5
  15. data/lib/charming/internal/terminal/tty_backend.rb +22 -2
  16. data/lib/charming/presentation/component.rb +3 -5
  17. data/lib/charming/presentation/components/activity_indicator.rb +173 -134
  18. data/lib/charming/presentation/components/command_palette.rb +94 -96
  19. data/lib/charming/presentation/components/command_palette_modal.rb +33 -0
  20. data/lib/charming/presentation/components/empty_state.rb +47 -49
  21. data/lib/charming/presentation/components/form/builder.rb +52 -54
  22. data/lib/charming/presentation/components/form/confirm.rb +49 -51
  23. data/lib/charming/presentation/components/form/field.rb +94 -96
  24. data/lib/charming/presentation/components/form/input.rb +53 -55
  25. data/lib/charming/presentation/components/form/note.rb +27 -29
  26. data/lib/charming/presentation/components/form/select.rb +84 -86
  27. data/lib/charming/presentation/components/form/textarea.rb +67 -69
  28. data/lib/charming/presentation/components/form.rb +120 -122
  29. data/lib/charming/presentation/components/keyboard_handler.rb +41 -43
  30. data/lib/charming/presentation/components/list.rb +123 -125
  31. data/lib/charming/presentation/components/markdown.rb +21 -23
  32. data/lib/charming/presentation/components/modal.rb +46 -48
  33. data/lib/charming/presentation/components/progressbar.rb +51 -53
  34. data/lib/charming/presentation/components/spinner.rb +40 -42
  35. data/lib/charming/presentation/components/table.rb +109 -111
  36. data/lib/charming/presentation/components/text_area.rb +219 -221
  37. data/lib/charming/presentation/components/text_input.rb +120 -122
  38. data/lib/charming/presentation/components/viewport.rb +218 -220
  39. data/lib/charming/presentation/layout/builder.rb +64 -66
  40. data/lib/charming/presentation/layout/overlay.rb +48 -50
  41. data/lib/charming/presentation/layout/pane.rb +122 -118
  42. data/lib/charming/presentation/layout/rect.rb +14 -16
  43. data/lib/charming/presentation/layout/screen_layout.rb +40 -42
  44. data/lib/charming/presentation/layout/split.rb +101 -103
  45. data/lib/charming/presentation/layout.rb +28 -30
  46. data/lib/charming/presentation/markdown/block_renderers.rb +94 -96
  47. data/lib/charming/presentation/markdown/inline_renderers.rb +52 -54
  48. data/lib/charming/presentation/markdown/render_context.rb +12 -14
  49. data/lib/charming/presentation/markdown/renderer.rb +84 -86
  50. data/lib/charming/presentation/markdown/syntax_highlighter.rb +57 -59
  51. data/lib/charming/presentation/markdown.rb +4 -6
  52. data/lib/charming/presentation/template_view.rb +22 -24
  53. data/lib/charming/presentation/templates/erb_handler.rb +4 -6
  54. data/lib/charming/presentation/templates.rb +47 -49
  55. data/lib/charming/presentation/ui/ansi_codes.rb +66 -68
  56. data/lib/charming/presentation/ui/ansi_slicer.rb +67 -69
  57. data/lib/charming/presentation/ui/border.rb +24 -26
  58. data/lib/charming/presentation/ui/border_painter.rb +37 -39
  59. data/lib/charming/presentation/ui/canvas.rb +59 -61
  60. data/lib/charming/presentation/ui/style.rb +173 -175
  61. data/lib/charming/presentation/ui/theme.rb +133 -135
  62. data/lib/charming/presentation/ui/width.rb +12 -14
  63. data/lib/charming/presentation/ui.rb +69 -71
  64. data/lib/charming/presentation/view.rb +103 -105
  65. data/lib/charming/runtime.rb +23 -10
  66. data/lib/charming/version.rb +1 -1
  67. data/lib/charming.rb +3 -2
  68. metadata +2 -1
@@ -1,119 +1,117 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Charming
4
- module Presentation
5
- module Components
6
- class Form
7
- # Field is the abstract base class for Form fields. Subclasses define `default_value`
8
- # and `render_control` (or override `render`); the base class supplies validation,
9
- # help-line rendering, error-line rendering, and value lookup against the form state.
10
- class Field < Component
11
- # The field's name symbol, human-readable label, optional help text, and bound state hash.
12
- attr_reader :name, :label, :help, :state
13
-
14
- # *name* is the value key (a Symbol). *label* defaults to a humanized version of *name*.
15
- # *required* enables a "is required" validator. *validate* is an optional callable
16
- # (returning nil/true ok, false "is invalid", Array messages, anything else → stringified).
17
- # *help* is an optional muted helper line rendered under the field.
18
- def initialize(name, label: nil, required: false, validate: nil, help: nil, theme: nil)
19
- super(theme: theme)
20
- @name = name.to_sym
21
- @label = label || humanize(name)
22
- @required = required
23
- @validator = validate
24
- @help = help
25
- end
4
+ module Components
5
+ class Form
6
+ # Field is the abstract base class for Form fields. Subclasses define `default_value`
7
+ # and `render_control` (or override `render`); the base class supplies validation,
8
+ # help-line rendering, error-line rendering, and value lookup against the form state.
9
+ class Field < Component
10
+ # The field's name symbol, human-readable label, optional help text, and bound state hash.
11
+ attr_reader :name, :label, :help, :state
12
+
13
+ # *name* is the value key (a Symbol). *label* defaults to a humanized version of *name*.
14
+ # *required* enables a "is required" validator. *validate* is an optional callable
15
+ # (returning nil/true ok, false → "is invalid", Array messages, anything else → stringified).
16
+ # *help* is an optional muted helper line rendered under the field.
17
+ def initialize(name, label: nil, required: false, validate: nil, help: nil, theme: nil)
18
+ super(theme: theme)
19
+ @name = name.to_sym
20
+ @label = label || humanize(name)
21
+ @required = required
22
+ @validator = validate
23
+ @help = help
24
+ end
26
25
 
27
- # Binds the field to the form's *state* hash, ensuring the field's per-field state
28
- # and a default value are present.
29
- def bind(state)
30
- @state = state
31
- state[:fields][name] ||= {}
32
- state[:values][name] = default_value unless state[:values].key?(name)
33
- end
26
+ # Binds the field to the form's *state* hash, ensuring the field's per-field state
27
+ # and a default value are present.
28
+ def bind(state)
29
+ @state = state
30
+ state[:fields][name] ||= {}
31
+ state[:values][name] = default_value unless state[:values].key?(name)
32
+ end
34
33
 
35
- # Subclasses that participate in Tab/Enter navigation return true. Default is true.
36
- def focusable?
37
- true
38
- end
34
+ # Subclasses that participate in Tab/Enter navigation return true. Default is true.
35
+ def focusable?
36
+ true
37
+ end
39
38
 
40
- # Default key handler returns nil (no key handling). Subclasses override.
41
- def handle_key(_event)
42
- nil
43
- end
39
+ # Default key handler returns nil (no key handling). Subclasses override.
40
+ def handle_key(_event)
41
+ nil
42
+ end
44
43
 
45
- # Renders the field as a control line prefixed with ">" (active) or " " (inactive),
46
- # optionally followed by the help line and any error lines.
47
- def render(active: false)
48
- line = "#{active ? ">" : " "} #{render_control}"
49
- line = theme.selected.render(line) if active
50
- [line, help_line, *error_lines].compact.join("\n")
51
- end
44
+ # Renders the field as a control line prefixed with ">" (active) or " " (inactive),
45
+ # optionally followed by the help line and any error lines.
46
+ def render(active: false)
47
+ line = "#{active ? ">" : " "} #{render_control}"
48
+ line = theme.selected.render(line) if active
49
+ [line, help_line, *error_lines].compact.join("\n")
50
+ end
52
51
 
53
- # Returns an array of validation error messages. Includes "is required" when
54
- # the field is required and blank, plus any messages produced by the user validator.
55
- def validate
56
- messages = []
57
- messages << "is required" if required? && blank?(value)
58
- messages.concat(validator_messages) if @validator
59
- messages
60
- end
52
+ # Returns an array of validation error messages. Includes "is required" when
53
+ # the field is required and blank, plus any messages produced by the user validator.
54
+ def validate
55
+ messages = []
56
+ messages << "is required" if required? && blank?(value)
57
+ messages.concat(validator_messages) if @validator
58
+ messages
59
+ end
61
60
 
62
- # The current value of this field from the bound state.
63
- def value
64
- state[:values][name]
65
- end
61
+ # The current value of this field from the bound state.
62
+ def value
63
+ state[:values][name]
64
+ end
66
65
 
67
- private
66
+ private
68
67
 
69
- # The default value assigned to a freshly-bound field. Subclasses override.
70
- def default_value
71
- nil
72
- end
68
+ # The default value assigned to a freshly-bound field. Subclasses override.
69
+ def default_value
70
+ nil
71
+ end
73
72
 
74
- # Renders the control portion (label + value). Default: "Label: <value>".
75
- def render_control
76
- "#{label}: #{value}"
77
- end
73
+ # Renders the control portion (label + value). Default: "Label: <value>".
74
+ def render_control
75
+ "#{label}: #{value}"
76
+ end
78
77
 
79
- # True when the field was declared with `required: true`.
80
- def required?
81
- @required
82
- end
78
+ # True when the field was declared with `required: true`.
79
+ def required?
80
+ @required
81
+ end
83
82
 
84
- # True when *value* is nil, an empty string, or responds to `empty?` with true.
85
- def blank?(value)
86
- return true if value.nil?
87
- return value.strip.empty? if value.is_a?(String)
83
+ # True when *value* is nil, an empty string, or responds to `empty?` with true.
84
+ def blank?(value)
85
+ return true if value.nil?
86
+ return value.strip.empty? if value.is_a?(String)
88
87
 
89
- value.respond_to?(:empty?) && value.empty?
90
- end
88
+ value.respond_to?(:empty?) && value.empty?
89
+ end
91
90
 
92
- # Normalizes the user validator's return value into an array of error message strings.
93
- def validator_messages
94
- result = @validator.call(value)
95
- case result
96
- when nil, true then []
97
- when false then ["is invalid"]
98
- when Array then result
99
- else [result.to_s]
100
- end
91
+ # Normalizes the user validator's return value into an array of error message strings.
92
+ def validator_messages
93
+ result = @validator.call(value)
94
+ case result
95
+ when nil, true then []
96
+ when false then ["is invalid"]
97
+ when Array then result
98
+ else [result.to_s]
101
99
  end
100
+ end
102
101
 
103
- # The muted help line (with two-space indent) when help text was given.
104
- def help_line
105
- " #{theme.muted.render(help)}" if help
106
- end
102
+ # The muted help line (with two-space indent) when help text was given.
103
+ def help_line
104
+ " #{theme.muted.render(help)}" if help
105
+ end
107
106
 
108
- # The list of error lines (with two-space indent) for any errors stored against this field.
109
- def error_lines
110
- Array(state[:errors][name]).map { |message| " #{theme.warn.render(message)}" }
111
- end
107
+ # The list of error lines (with two-space indent) for any errors stored against this field.
108
+ def error_lines
109
+ Array(state[:errors][name]).map { |message| " #{theme.warn.render(message)}" }
110
+ end
112
111
 
113
- # Converts a snake_case symbol/string to a humanized "Capitalized" string.
114
- def humanize(value)
115
- value.to_s.tr("_", " ").capitalize
116
- end
112
+ # Converts a snake_case symbol/string to a humanized "Capitalized" string.
113
+ def humanize(value)
114
+ value.to_s.tr("_", " ").capitalize
117
115
  end
118
116
  end
119
117
  end
@@ -1,69 +1,67 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Charming
4
- module Presentation
5
- module Components
6
- class Form
7
- # Input is a single-line Form field backed by a TextInput widget. The cursor position
8
- # is persisted in the form's per-field state so the field can be refocused mid-edit.
9
- class Input < Field
10
- # *value* is the initial text. *placeholder* is shown when the value is empty.
11
- # *width* optionally constrains the rendered width. All other options are forwarded
12
- # to Field (label, required, validate, help, theme).
13
- def initialize(name, value: "", placeholder: "", width: nil, **options)
14
- super(name, **options)
15
- @initial_value = value
16
- @placeholder = placeholder
17
- @width = width
18
- end
4
+ module Components
5
+ class Form
6
+ # Input is a single-line Form field backed by a TextInput widget. The cursor position
7
+ # is persisted in the form's per-field state so the field can be refocused mid-edit.
8
+ class Input < Field
9
+ # *value* is the initial text. *placeholder* is shown when the value is empty.
10
+ # *width* optionally constrains the rendered width. All other options are forwarded
11
+ # to Field (label, required, validate, help, theme).
12
+ def initialize(name, value: "", placeholder: "", width: nil, **options)
13
+ super(name, **options)
14
+ @initial_value = value
15
+ @placeholder = placeholder
16
+ @width = width
17
+ end
19
18
 
20
- # Binds the field to the form state, sets the initial value if absent, and initializes
21
- # the per-field cursor offset to the end of the value.
22
- def bind(state)
23
- super
24
- state[:values][name] = @initial_value if state[:values][name].nil?
25
- field_state[:cursor] = state[:values][name].to_s.length unless field_state.key?(:cursor)
26
- end
19
+ # Binds the field to the form state, sets the initial value if absent, and initializes
20
+ # the per-field cursor offset to the end of the value.
21
+ def bind(state)
22
+ super
23
+ state[:values][name] = @initial_value if state[:values][name].nil?
24
+ field_state[:cursor] = state[:values][name].to_s.length unless field_state.key?(:cursor)
25
+ end
27
26
 
28
- # Forwards key events to the underlying TextInput, syncing the value and cursor
29
- # back into the form state. Returns :handled when the event was consumed.
30
- def handle_key(event)
31
- text_input = input
32
- result = text_input.handle_key(event)
33
- return nil unless result == :handled
27
+ # Forwards key events to the underlying TextInput, syncing the value and cursor
28
+ # back into the form state. Returns :handled when the event was consumed.
29
+ def handle_key(event)
30
+ text_input = input
31
+ result = text_input.handle_key(event)
32
+ return nil unless result == :handled
34
33
 
35
- state[:values][name] = text_input.value
36
- field_state[:cursor] = text_input.cursor
37
- :handled
38
- end
34
+ state[:values][name] = text_input.value
35
+ field_state[:cursor] = text_input.cursor
36
+ :handled
37
+ end
39
38
 
40
- private
39
+ private
41
40
 
42
- # The default value for a freshly-bound field is the *value* passed at construction.
43
- def default_value
44
- @initial_value
45
- end
41
+ # The default value for a freshly-bound field is the *value* passed at construction.
42
+ def default_value
43
+ @initial_value
44
+ end
46
45
 
47
- # Renders the field as "Label: <text input>".
48
- def render_control
49
- "#{label}: #{input.render}"
50
- end
46
+ # Renders the field as "Label: <text input>".
47
+ def render_control
48
+ "#{label}: #{input.render}"
49
+ end
51
50
 
52
- # Builds a fresh TextInput each render, seeded from the current form-state value
53
- # and the persisted cursor offset.
54
- def input
55
- TextInput.new(
56
- value: value.to_s,
57
- placeholder: @placeholder,
58
- width: @width,
59
- cursor: field_state[:cursor]
60
- )
61
- end
51
+ # Builds a fresh TextInput each render, seeded from the current form-state value
52
+ # and the persisted cursor offset.
53
+ def input
54
+ TextInput.new(
55
+ value: value.to_s,
56
+ placeholder: @placeholder,
57
+ width: @width,
58
+ cursor: field_state[:cursor]
59
+ )
60
+ end
62
61
 
63
- # Returns the per-field state hash for this field.
64
- def field_state
65
- state[:fields][name]
66
- end
62
+ # Returns the per-field state hash for this field.
63
+ def field_state
64
+ state[:fields][name]
67
65
  end
68
66
  end
69
67
  end
@@ -1,39 +1,37 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Charming
4
- module Presentation
5
- module Components
6
- class Form
7
- # Note is a non-interactive Form field that renders a static string of text. Notes
8
- # never receive focus, never validate, and store no value they are presentational
9
- # only, useful for headings, dividers, or instructional text inside a form.
10
- class Note < Field
11
- # *text* is the literal string to render. *name* is unused (defaults to :note) and
12
- # exists only because the Field base class requires a name.
13
- def initialize(text, name: :note, theme: nil)
14
- super(name, theme: theme)
15
- @text = text
16
- end
4
+ module Components
5
+ class Form
6
+ # Note is a non-interactive Form field that renders a static string of text. Notes
7
+ # never receive focus, never validate, and store no value they are presentational
8
+ # only, useful for headings, dividers, or instructional text inside a form.
9
+ class Note < Field
10
+ # *text* is the literal string to render. *name* is unused (defaults to :note) and
11
+ # exists only because the Field base class requires a name.
12
+ def initialize(text, name: :note, theme: nil)
13
+ super(name, theme: theme)
14
+ @text = text
15
+ end
17
16
 
18
- # Binds the field to the form state but does not create any per-field storage.
19
- def bind(state)
20
- @state = state
21
- end
17
+ # Binds the field to the form state but does not create any per-field storage.
18
+ def bind(state)
19
+ @state = state
20
+ end
22
21
 
23
- # Notes are never focusable and therefore excluded from Tab/Enter traversal.
24
- def focusable?
25
- false
26
- end
22
+ # Notes are never focusable and therefore excluded from Tab/Enter traversal.
23
+ def focusable?
24
+ false
25
+ end
27
26
 
28
- # Notes never produce validation errors.
29
- def validate
30
- []
31
- end
27
+ # Notes never produce validation errors.
28
+ def validate
29
+ []
30
+ end
32
31
 
33
- # Returns the literal text, ignoring the *active:* flag (notes have no focus state).
34
- def render(active: false)
35
- @text.to_s
36
- end
32
+ # Returns the literal text, ignoring the *active:* flag (notes have no focus state).
33
+ def render(active: false)
34
+ @text.to_s
37
35
  end
38
36
  end
39
37
  end
@@ -1,110 +1,108 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Charming
4
- module Presentation
5
- module Components
6
- class Form
7
- # Select is a single-choice Form field backed by a List widget. The selected option
8
- # becomes the field's value; navigation keys (up/down/home/end) cycle through options
9
- # and Enter has no effect (selection is applied immediately on key release).
10
- class Select < Field
11
- # *options* is the array of selectable values. *selected_index* defaults to 0.
12
- # *option_label* is a callable used to extract the display string (default: `to_s`).
13
- # All other options are forwarded to Field.
14
- def initialize(name, options:, selected_index: 0, option_label: :to_s.to_proc, **field_options)
15
- super(name, **field_options)
16
- @options = options
17
- @initial_selected_index = selected_index
18
- @option_label = option_label
19
- end
4
+ module Components
5
+ class Form
6
+ # Select is a single-choice Form field backed by a List widget. The selected option
7
+ # becomes the field's value; navigation keys (up/down/home/end) cycle through options
8
+ # and Enter has no effect (selection is applied immediately on key release).
9
+ class Select < Field
10
+ # *options* is the array of selectable values. *selected_index* defaults to 0.
11
+ # *option_label* is a callable used to extract the display string (default: `to_s`).
12
+ # All other options are forwarded to Field.
13
+ def initialize(name, options:, selected_index: 0, option_label: :to_s.to_proc, **field_options)
14
+ super(name, **field_options)
15
+ @options = options
16
+ @initial_selected_index = selected_index
17
+ @option_label = option_label
18
+ end
20
19
 
21
- # Binds the field, then ensures the persisted selection (or initial/derived one) is applied.
22
- def bind(state)
23
- super
24
- ensure_selection
25
- end
20
+ # Binds the field, then ensures the persisted selection (or initial/derived one) is applied.
21
+ def bind(state)
22
+ super
23
+ ensure_selection
24
+ end
26
25
 
27
- # Forwards key events to the underlying List, syncing the chosen option index back
28
- # into the field state. Returns :handled when consumed.
29
- def handle_key(event)
30
- selection = list
31
- result = selection.handle_key(event)
32
- return nil unless result == :handled
26
+ # Forwards key events to the underlying List, syncing the chosen option index back
27
+ # into the field state. Returns :handled when consumed.
28
+ def handle_key(event)
29
+ selection = list
30
+ result = selection.handle_key(event)
31
+ return nil unless result == :handled
33
32
 
34
- save_selection(selection.selected_index)
35
- :handled
36
- end
33
+ save_selection(selection.selected_index)
34
+ :handled
35
+ end
37
36
 
38
- private
37
+ private
39
38
 
40
- # The options array (used as the source of truth for default value and clamp).
41
- attr_reader :options
39
+ # The options array (used as the source of truth for default value and clamp).
40
+ attr_reader :options
42
41
 
43
- # The default value is the option at the clamped initial selected index.
44
- def default_value
45
- options[clamped_initial_index]
46
- end
42
+ # The default value is the option at the clamped initial selected index.
43
+ def default_value
44
+ options[clamped_initial_index]
45
+ end
47
46
 
48
- # Renders the field as "Label: <display value>".
49
- def render_control
50
- "#{label}: #{display_value}"
51
- end
47
+ # Renders the field as "Label: <display value>".
48
+ def render_control
49
+ "#{label}: #{display_value}"
50
+ end
52
51
 
53
- # Returns the stringified value via the configured option label callable.
54
- def display_value
55
- value.nil? ? "" : @option_label.call(value)
56
- end
52
+ # Returns the stringified value via the configured option label callable.
53
+ def display_value
54
+ value.nil? ? "" : @option_label.call(value)
55
+ end
57
56
 
58
- # Builds a fresh List each render with the current options, selected index, label
59
- # callable, and theme.
60
- def list
61
- List.new(items: options, selected_index: selected_index, label: @option_label, theme: theme)
62
- end
57
+ # Builds a fresh List each render with the current options, selected index, label
58
+ # callable, and theme.
59
+ def list
60
+ List.new(items: options, selected_index: selected_index, label: @option_label, theme: theme)
61
+ end
63
62
 
64
- # Ensures the persisted selection is set, falling back to the field's initial index
65
- # or the current stored value.
66
- def ensure_selection
67
- if field_state.key?(:selected_index)
68
- save_selection(field_state[:selected_index])
69
- elsif state[:values].key?(name)
70
- save_selection(index_for(state[:values][name]) || clamped_initial_index)
71
- else
72
- save_selection(clamped_initial_index)
73
- end
63
+ # Ensures the persisted selection is set, falling back to the field's initial index
64
+ # or the current stored value.
65
+ def ensure_selection
66
+ if field_state.key?(:selected_index)
67
+ save_selection(field_state[:selected_index])
68
+ elsif state[:values].key?(name)
69
+ save_selection(index_for(state[:values][name]) || clamped_initial_index)
70
+ else
71
+ save_selection(clamped_initial_index)
74
72
  end
73
+ end
75
74
 
76
- # Persists the chosen *index* and the corresponding option as the field's value.
77
- def save_selection(index)
78
- field_state[:selected_index] = clamp_index(index)
79
- state[:values][name] = options[field_state[:selected_index]]
80
- end
75
+ # Persists the chosen *index* and the corresponding option as the field's value.
76
+ def save_selection(index)
77
+ field_state[:selected_index] = clamp_index(index)
78
+ state[:values][name] = options[field_state[:selected_index]]
79
+ end
81
80
 
82
- # The currently persisted selected index (or the initial index when unset).
83
- def selected_index
84
- field_state[:selected_index] || clamped_initial_index
85
- end
81
+ # The currently persisted selected index (or the initial index when unset).
82
+ def selected_index
83
+ field_state[:selected_index] || clamped_initial_index
84
+ end
86
85
 
87
- # Clamps the initial selected index to the valid range.
88
- def clamped_initial_index
89
- clamp_index(@initial_selected_index)
90
- end
86
+ # Clamps the initial selected index to the valid range.
87
+ def clamped_initial_index
88
+ clamp_index(@initial_selected_index)
89
+ end
91
90
 
92
- # Clamps *index* to the valid range. Returns 0 when there are no options.
93
- def clamp_index(index)
94
- return 0 if options.empty?
91
+ # Clamps *index* to the valid range. Returns 0 when there are no options.
92
+ def clamp_index(index)
93
+ return 0 if options.empty?
95
94
 
96
- index.to_i.clamp(0, options.length - 1)
97
- end
95
+ index.to_i.clamp(0, options.length - 1)
96
+ end
98
97
 
99
- # Returns the index of *option* in the options array, or nil when absent.
100
- def index_for(option)
101
- options.index(option)
102
- end
98
+ # Returns the index of *option* in the options array, or nil when absent.
99
+ def index_for(option)
100
+ options.index(option)
101
+ end
103
102
 
104
- # Returns the per-field state hash for this field.
105
- def field_state
106
- state[:fields][name]
107
- end
103
+ # Returns the per-field state hash for this field.
104
+ def field_state
105
+ state[:fields][name]
108
106
  end
109
107
  end
110
108
  end