charming 0.1.2 → 0.1.4

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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/lib/charming/application.rb +22 -5
  3. data/lib/charming/cli.rb +3 -3
  4. data/lib/charming/controller/class_methods.rb +2 -2
  5. data/lib/charming/controller/command_palette.rb +2 -2
  6. data/lib/charming/controller/component_dispatching.rb +47 -3
  7. data/lib/charming/controller/focus.rb +123 -0
  8. data/lib/charming/controller/focus_management.rb +1 -1
  9. data/lib/charming/controller/rendering.rb +6 -17
  10. data/lib/charming/controller/session_state.rb +12 -1
  11. data/lib/charming/controller.rb +11 -2
  12. data/lib/charming/database/commands.rb +106 -0
  13. data/lib/charming/generators/component_generator.rb +1 -1
  14. data/lib/charming/generators/database_installer.rb +154 -0
  15. data/lib/charming/generators/model_generator.rb +2 -10
  16. data/lib/charming/generators/name.rb +1 -1
  17. data/lib/charming/generators/templates/app/application.template +1 -1
  18. data/lib/charming/generators/templates/app/layout.template +3 -6
  19. data/lib/charming/generators/templates/app/view.template +1 -1
  20. data/lib/charming/generators/templates/component/component.rb.template +1 -1
  21. data/lib/charming/generators/templates/screen/view.rb.template +1 -1
  22. data/lib/charming/generators/templates/view/view.rb.template +1 -1
  23. data/lib/charming/generators/view_generator.rb +1 -1
  24. data/lib/charming/internal/renderer/differential.rb +13 -5
  25. data/lib/charming/internal/terminal/tty_backend.rb +22 -2
  26. data/lib/charming/presentation/component.rb +3 -5
  27. data/lib/charming/presentation/components/activity_indicator.rb +173 -134
  28. data/lib/charming/presentation/components/command_palette.rb +94 -96
  29. data/lib/charming/presentation/components/command_palette_modal.rb +33 -0
  30. data/lib/charming/presentation/components/empty_state.rb +47 -49
  31. data/lib/charming/presentation/components/form/builder.rb +52 -54
  32. data/lib/charming/presentation/components/form/confirm.rb +49 -51
  33. data/lib/charming/presentation/components/form/field.rb +94 -96
  34. data/lib/charming/presentation/components/form/input.rb +53 -55
  35. data/lib/charming/presentation/components/form/note.rb +27 -29
  36. data/lib/charming/presentation/components/form/select.rb +84 -86
  37. data/lib/charming/presentation/components/form/textarea.rb +67 -69
  38. data/lib/charming/presentation/components/form.rb +120 -122
  39. data/lib/charming/presentation/components/keyboard_handler.rb +41 -43
  40. data/lib/charming/presentation/components/list.rb +123 -125
  41. data/lib/charming/presentation/components/markdown.rb +21 -23
  42. data/lib/charming/presentation/components/modal.rb +46 -48
  43. data/lib/charming/presentation/components/progressbar.rb +51 -53
  44. data/lib/charming/presentation/components/spinner.rb +40 -42
  45. data/lib/charming/presentation/components/table.rb +109 -111
  46. data/lib/charming/presentation/components/text_area.rb +219 -221
  47. data/lib/charming/presentation/components/text_input.rb +120 -122
  48. data/lib/charming/presentation/components/viewport.rb +218 -220
  49. data/lib/charming/presentation/layout/builder.rb +64 -66
  50. data/lib/charming/presentation/layout/overlay.rb +48 -50
  51. data/lib/charming/presentation/layout/pane.rb +129 -118
  52. data/lib/charming/presentation/layout/rect.rb +19 -16
  53. data/lib/charming/presentation/layout/screen_layout.rb +47 -42
  54. data/lib/charming/presentation/layout/split.rb +107 -102
  55. data/lib/charming/presentation/layout.rb +28 -30
  56. data/lib/charming/presentation/markdown/render_context.rb +30 -14
  57. data/lib/charming/presentation/markdown/renderer.rb +302 -79
  58. data/lib/charming/presentation/markdown/style_config.rb +215 -0
  59. data/lib/charming/presentation/markdown/syntax_highlighter.rb +58 -59
  60. data/lib/charming/presentation/markdown.rb +4 -6
  61. data/lib/charming/presentation/template_view.rb +22 -24
  62. data/lib/charming/presentation/templates/erb_handler.rb +4 -6
  63. data/lib/charming/presentation/templates.rb +47 -49
  64. data/lib/charming/presentation/ui/ansi_codes.rb +66 -68
  65. data/lib/charming/presentation/ui/ansi_slicer.rb +67 -69
  66. data/lib/charming/presentation/ui/border.rb +24 -26
  67. data/lib/charming/presentation/ui/border_painter.rb +37 -39
  68. data/lib/charming/presentation/ui/canvas.rb +59 -61
  69. data/lib/charming/presentation/ui/style.rb +173 -175
  70. data/lib/charming/presentation/ui/theme.rb +133 -135
  71. data/lib/charming/presentation/ui/width.rb +12 -14
  72. data/lib/charming/presentation/ui.rb +69 -71
  73. data/lib/charming/presentation/view.rb +110 -105
  74. data/lib/charming/router.rb +3 -8
  75. data/lib/charming/runtime.rb +25 -10
  76. data/lib/charming/version.rb +1 -1
  77. data/lib/charming.rb +5 -4
  78. metadata +43 -9
  79. data/lib/charming/database_commands.rb +0 -103
  80. data/lib/charming/database_installer.rb +0 -152
  81. data/lib/charming/focus.rb +0 -121
  82. data/lib/charming/presentation/markdown/block_renderers.rb +0 -120
  83. data/lib/charming/presentation/markdown/inline_renderers.rb +0 -68
@@ -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
@@ -1,84 +1,82 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Charming
4
- module Presentation
5
- module Components
6
- class Form
7
- # Textarea is a multi-line Form field backed by a TextArea widget. The cursor offset,
8
- # top-visible row, and preferred vertical column are all persisted in the form's
9
- # per-field state so the field behaves consistently when refocused mid-edit.
10
- class Textarea < Field
11
- # *value* is the initial text. *placeholder* is shown when the value is empty.
12
- # *width* and *height* constrain the rendered area. All other options are forwarded
13
- # to Field (label, required, validate, help, theme).
14
- def initialize(name, value: "", placeholder: "", width: nil, height: nil, **options)
15
- super(name, **options)
16
- @initial_value = value
17
- @placeholder = placeholder
18
- @width = width
19
- @height = height
20
- end
4
+ module Components
5
+ class Form
6
+ # Textarea is a multi-line Form field backed by a TextArea widget. The cursor offset,
7
+ # top-visible row, and preferred vertical column are all persisted in the form's
8
+ # per-field state so the field behaves consistently when refocused mid-edit.
9
+ class Textarea < Field
10
+ # *value* is the initial text. *placeholder* is shown when the value is empty.
11
+ # *width* and *height* constrain the rendered area. All other options are forwarded
12
+ # to Field (label, required, validate, help, theme).
13
+ def initialize(name, value: "", placeholder: "", width: nil, height: nil, **options)
14
+ super(name, **options)
15
+ @initial_value = value
16
+ @placeholder = placeholder
17
+ @width = width
18
+ @height = height
19
+ end
21
20
 
22
- # Binds the field, seeds the initial value, and initializes the cursor/offset state.
23
- def bind(state)
24
- super
25
- state[:values][name] = @initial_value if state[:values][name].nil?
26
- field_state[:cursor] = state[:values][name].to_s.length unless field_state.key?(:cursor)
27
- field_state[:offset] ||= 0
28
- end
21
+ # Binds the field, seeds the initial value, and initializes the cursor/offset state.
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
+ field_state[:offset] ||= 0
27
+ end
29
28
 
30
- # Forwards key events to the underlying TextArea, syncing the value, cursor, offset,
31
- # and preferred column back into the form state. Returns :handled when consumed.
32
- def handle_key(event)
33
- area = text_area
34
- result = area.handle_key(event)
35
- return nil unless result == :handled
29
+ # Forwards key events to the underlying TextArea, syncing the value, cursor, offset,
30
+ # and preferred column back into the form state. Returns :handled when consumed.
31
+ def handle_key(event)
32
+ area = text_area
33
+ result = area.handle_key(event)
34
+ return nil unless result == :handled
36
35
 
37
- state[:values][name] = area.value
38
- field_state[:cursor] = area.cursor
39
- field_state[:offset] = area.offset
40
- field_state[:preferred_column] = area.preferred_column
41
- :handled
42
- end
36
+ state[:values][name] = area.value
37
+ field_state[:cursor] = area.cursor
38
+ field_state[:offset] = area.offset
39
+ field_state[:preferred_column] = area.preferred_column
40
+ :handled
41
+ end
43
42
 
44
- # Renders the field with its label on the first line, body lines indented, and
45
- # optional help/error lines below.
46
- def render(active: false)
47
- label_line = "#{active ? ">" : " "} #{label}:"
48
- label_line = theme.selected.render(label_line) if active
49
- [label_line, *body_lines, help_line, *error_lines].compact.join("\n")
50
- end
43
+ # Renders the field with its label on the first line, body lines indented, and
44
+ # optional help/error lines below.
45
+ def render(active: false)
46
+ label_line = "#{active ? ">" : " "} #{label}:"
47
+ label_line = theme.selected.render(label_line) if active
48
+ [label_line, *body_lines, help_line, *error_lines].compact.join("\n")
49
+ end
51
50
 
52
- private
51
+ private
53
52
 
54
- # The default value for a freshly-bound field is the *value* passed at construction.
55
- def default_value
56
- @initial_value
57
- end
53
+ # The default value for a freshly-bound field is the *value* passed at construction.
54
+ def default_value
55
+ @initial_value
56
+ end
58
57
 
59
- # Renders the multi-line body, indenting each line by two spaces.
60
- def body_lines
61
- text_area.render.lines(chomp: true).map { |line| " #{line}" }
62
- end
58
+ # Renders the multi-line body, indenting each line by two spaces.
59
+ def body_lines
60
+ text_area.render.lines(chomp: true).map { |line| " #{line}" }
61
+ end
63
62
 
64
- # Builds a fresh TextArea each render, seeded from the current form-state value and
65
- # the persisted cursor/offset/preferred_column.
66
- def text_area
67
- TextArea.new(
68
- value: value.to_s,
69
- placeholder: @placeholder,
70
- width: @width,
71
- height: @height,
72
- cursor: field_state[:cursor],
73
- offset: field_state[:offset],
74
- preferred_column: field_state[:preferred_column]
75
- )
76
- end
63
+ # Builds a fresh TextArea each render, seeded from the current form-state value and
64
+ # the persisted cursor/offset/preferred_column.
65
+ def text_area
66
+ TextArea.new(
67
+ value: value.to_s,
68
+ placeholder: @placeholder,
69
+ width: @width,
70
+ height: @height,
71
+ cursor: field_state[:cursor],
72
+ offset: field_state[:offset],
73
+ preferred_column: field_state[:preferred_column]
74
+ )
75
+ end
77
76
 
78
- # Returns the per-field state hash for this field.
79
- def field_state
80
- state[:fields][name]
81
- end
77
+ # Returns the per-field state hash for this field.
78
+ def field_state
79
+ state[:fields][name]
82
80
  end
83
81
  end
84
82
  end