glimmer-dsl-libui 0.12.1 → 0.12.2

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: 9dc091cfefc95ef7b7f93457fc609a3ee852912d5c72fb79a363c9dd96787865
4
- data.tar.gz: d6547ce1dae1fa9abb73bdcf7b441b087f0bc7ab66dd0d8eedeed724e82c810f
3
+ metadata.gz: 467e3450aca13a338d5d835aa79c7b483cd9475866d6e6f08e2b130386107d1a
4
+ data.tar.gz: 81674d7b0b95d973b5286c1c8cc187e747412e02a6a0f7bffc7d235b499186c6
5
5
  SHA512:
6
- metadata.gz: 352489b82a6706cbc9232f21be57a1e236efc4b08d76653b4b2fce73b0e931c6d3c7e10207f14f2f2417336f6f54bcdb03b52aa84ec55e8c9849be2bf2bfb361
7
- data.tar.gz: 23587a71abcecc6d95bdc471b3fb907928abb028f439aaf22542fc2c19bf26916944e35770e36f1600765e04c767c0a512d5626cfbf2a0ebe007286a5516b733
6
+ metadata.gz: ada3fc2a2db26d4660876010f1b8f0cdc08797baa0b0e63e5a044e050dc9cf400f6318dc7e09328137b755c861695c1a74901ec91be2add91433c1d487ba1438
7
+ data.tar.gz: f1aee5106ee4500c46970de47136da231c4e096a213f65095cb2640e8b978672100ef036ebb8a24fbd65137ec222f84a345832c6fe69bc87066599496658c8b6
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Change Log
2
2
 
3
+ ## 0.12.2
4
+
5
+ - New `code_entry` control [ALPHA/EXPERIMENTAL/INCOMPLETE/UNSTABLE] (similar to `code_area`, but with code editing functionality).
6
+ - New examples/basic_code_entry.rb
7
+ - Update examples/basic_code_area.rb to have padding inside the `code_area` and no window padding anymore
8
+ - Have `code_area` handle italic syntax highlighting styles
9
+ - Support `code_area` `font_family` and `font_size` options
10
+ - Controls prepend `Parent` mixin to keep track of their children via `children` attribute
11
+ - tab_item always generates a vertical_box root container (root_proxy) before adding its child content within it (also, it does not generate a horizontal_box anymore by default if it had no content to start, yet a vertical_box all the time)
12
+ - Upgrade to glimmer 2.7.9
13
+ - Fix issue in `code_area` not recognizing indentation
14
+
3
15
  ## 0.12.1
4
16
 
5
17
  - Fix issue with getting error "superclass mismatch for class Array" when `concurrent-ruby` is included in a desktop applications as it is required to load `concurrent-ruby` before `glimmer`, so now `glimmer-dsl-libui` will attempt loading `concurrent-ruby` if available or default to old behavior otherwise.
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # [<img src="https://raw.githubusercontent.com/AndyObtiva/glimmer/master/images/glimmer-logo-hi-res.png" height=85 />](https://github.com/AndyObtiva/glimmer) Glimmer DSL for LibUI 0.12.1
1
+ # [<img src="https://raw.githubusercontent.com/AndyObtiva/glimmer/master/images/glimmer-logo-hi-res.png" height=85 />](https://github.com/AndyObtiva/glimmer) Glimmer DSL for LibUI 0.12.2
2
2
  ## Prerequisite-Free Ruby Desktop Development Cross-Platform Native GUI Library ([Fukuoka Award Winning](http://www.digitalfukuoka.jp/topics/187?locale=ja))
3
3
  ### The Quickest Way From Zero To GUI
4
4
  [![Gem Version](https://badge.fury.io/rb/glimmer-dsl-libui.svg)](http://badge.fury.io/rb/glimmer-dsl-libui)
@@ -456,7 +456,7 @@ gem install glimmer-dsl-libui
456
456
  Or install via Bundler `Gemfile`:
457
457
 
458
458
  ```ruby
459
- gem 'glimmer-dsl-libui', '~> 0.12.1'
459
+ gem 'glimmer-dsl-libui', '~> 0.12.2'
460
460
  ```
461
461
 
462
462
  Test that installation worked by running the [Glimmer Meta-Example](#examples):
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.12.1
1
+ 0.12.2
@@ -7,7 +7,6 @@ class BasicCodeArea
7
7
  @code = <<~CODE
8
8
  # Greets target with greeting
9
9
  def greet(greeting: 'Hello', target: 'World')
10
-
11
10
  puts "\#{greeting}, \#{target}!"
12
11
  end
13
12
 
@@ -21,8 +20,6 @@ class BasicCodeArea
21
20
 
22
21
  body {
23
22
  window('Basic Code Area', 400, 300) {
24
- margined true
25
-
26
23
  code_area(language: 'ruby', code: @code)
27
24
  }
28
25
  }
@@ -0,0 +1,30 @@
1
+ require 'glimmer-dsl-libui'
2
+
3
+ # code_entry is experimental/incomplete and not recommended for serious usage
4
+ class BasicCodeEntry
5
+ include Glimmer::LibUI::Application
6
+
7
+ before_body do
8
+ @code = <<~CODE
9
+ # Greets target with greeting
10
+ def greet(greeting: 'Hello', target: 'World')
11
+ puts "\#{greeting}, \#{target}!"
12
+ end
13
+
14
+ greet
15
+ greet(target: 'Robert')
16
+ greet(greeting: 'Aloha')
17
+ greet(greeting: 'Aloha', target: 'Nancy')
18
+ greet(greeting: 'Howdy', target: 'Doodle')
19
+ CODE
20
+ end
21
+
22
+ body {
23
+ window('Basic Code Entry', 400, 300) {
24
+ # code_entry is experimental/incomplete and not recommended for serious usage
25
+ code_entry(language: 'ruby', code: @code)
26
+ }
27
+ }
28
+ end
29
+
30
+ BasicCodeEntry.launch
Binary file
@@ -29,17 +29,18 @@ module Glimmer
29
29
  #
30
30
  # Follows the Proxy Design Pattern
31
31
  class TabItemProxy < ControlProxy
32
- attr_reader :index
32
+ attr_reader :index, :root_proxy
33
33
 
34
34
  def initialize(keyword, parent, args, &block)
35
35
  @keyword = keyword
36
36
  @parent_proxy = parent
37
+ @root_proxy = nil
37
38
  @args = args
38
39
  @block = block
39
40
  @enabled = 1
40
41
  @index = @parent_proxy.num_pages
41
- @content = @block&.call
42
42
  build_control
43
+ @content = @root_proxy.content(&@block) if @block
43
44
  end
44
45
 
45
46
  def name
@@ -57,11 +58,18 @@ module Glimmer
57
58
  alias margined= margined
58
59
  alias margined? margined
59
60
 
61
+ # TODO implement tab_insert_at
62
+
63
+ def destroy
64
+ @parent_proxy.delete(@index)
65
+ end
66
+
60
67
  private
61
68
 
62
69
  def build_control
63
- @content = Box::HorizontalBoxProxy.new('horizontal_box', @libui, []) if @content.nil?
64
- @libui = @parent_proxy.append(name, @content.libui)
70
+ @root_proxy = Box::VerticalBoxProxy.new('vertical_box', @parent_proxy, [])
71
+ # TODO support being able to re-open a tab item and add content.. it needs special code.. perhaps by having a @content parent for every tab item, this problem becomes easier to solve
72
+ @parent_proxy.append(name, @root_proxy.libui) # returns nil, so there is no @libui object to store in this case
65
73
  end
66
74
  end
67
75
  end
@@ -203,7 +203,7 @@ module Glimmer
203
203
  def extent_width
204
204
  if @extent_width.to_f > 0
205
205
  @extent_width
206
- else
206
+ else # TODO auto calculate extents
207
207
  width
208
208
  end
209
209
  end
@@ -211,7 +211,7 @@ module Glimmer
211
211
  def extent_height
212
212
  if @extent_height.to_f > 0
213
213
  @extent_height
214
- else
214
+ else # TODO auto calculate extents
215
215
  children_max_size = children.map(&:font).map {|font| font[:size] if font.respond_to?(:[]) }.compact.max
216
216
  if children_max_size.to_f > 0
217
217
  children_max_size
@@ -221,6 +221,16 @@ module Glimmer
221
221
  end
222
222
  end
223
223
 
224
+ def calculate_extents
225
+ # TODO fix implementation once libui binding project responds about this
226
+ # as it always returns 0,0 right now
227
+ extent_width = Fiddle::Pointer.malloc(Fiddle::SIZEOF_DOUBLE*8)
228
+ extent_height = Fiddle::Pointer.malloc(Fiddle::SIZEOF_DOUBLE*8)
229
+ ::LibUI.draw_text_layout_extents(@libui, extent_width, extent_height)
230
+ @extent_width = extent_width[0, Fiddle::SIZEOF_DOUBLE*8].unpack1('i')
231
+ @extent_height = extent_height[0, Fiddle::SIZEOF_DOUBLE*8].unpack1('i')
232
+ end
233
+
224
234
  private
225
235
 
226
236
  def build_control
@@ -234,16 +244,6 @@ module Glimmer
234
244
  draw_brush.B = (draw_brush_args[:b] || draw_brush_args[:blue]).to_f / 255.0
235
245
  draw_brush.A = (draw_brush_args[:a] || draw_brush_args[:alpha])
236
246
  end
237
-
238
- def calculate_extents
239
- # TODO fix implementation once libui binding project responds about this
240
- # as it always returns 0,0 right now
241
- extent_width = Fiddle::Pointer.malloc(Fiddle::SIZEOF_DOUBLE*8)
242
- extent_height = Fiddle::Pointer.malloc(Fiddle::SIZEOF_DOUBLE*8)
243
- ::LibUI.draw_text_layout_extents(@libui, extent_width, extent_height)
244
- @extent_width = extent_width[0, Fiddle::SIZEOF_DOUBLE*8].unpack1('i')
245
- @extent_height = extent_height[0, Fiddle::SIZEOF_DOUBLE*8].unpack1('i')
246
- end
247
247
  end
248
248
  end
249
249
  end
@@ -20,6 +20,7 @@
20
20
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
21
 
22
22
  require 'glimmer/libui/data_bindable'
23
+ require 'glimmer/libui/parent'
23
24
 
24
25
  module Glimmer
25
26
  module LibUI
@@ -104,6 +105,7 @@ module Glimmer
104
105
  end
105
106
 
106
107
  include DataBindable
108
+ prepend Parent
107
109
 
108
110
  KEYWORD_ALIASES = {
109
111
  'message_box' => 'msg_box',
@@ -354,6 +356,7 @@ module Glimmer
354
356
  deregister_all_custom_listeners
355
357
  send_to_libui('destroy')
356
358
  ControlProxy.control_proxies.delete(self)
359
+ # TODO should we destroy all children too or at least remove them from the children collection?
357
360
  end
358
361
 
359
362
  def enabled(value = nil)
@@ -19,24 +19,15 @@
19
19
  # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
20
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
21
 
22
+ require 'os'
23
+
22
24
  require 'glimmer/libui/custom_control'
25
+ require 'glimmer/libui/syntax_highlighter'
23
26
 
24
27
  module Glimmer
25
28
  module LibUI
26
29
  module CustomControl
27
30
  class CodeArea
28
- class << self
29
- def languages
30
- require 'rouge'
31
- Rouge::Lexer.all.map {|lexer| lexer.tag}.sort
32
- end
33
-
34
- def lexers
35
- require 'rouge'
36
- Rouge::Lexer.all.sort_by(&:title)
37
- end
38
- end
39
-
40
31
  include Glimmer::LibUI::CustomControl
41
32
 
42
33
  REGEX_COLOR_HEX6 = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/
@@ -44,50 +35,38 @@ module Glimmer
44
35
  option :language, default: 'ruby'
45
36
  option :theme, default: 'glimmer'
46
37
  option :code
38
+ option :padding, default: 10
39
+ option :font_family, default: OS.mac? ? 'Consolas' : 'Courier'
40
+ option :font_size, default: 13
41
+
42
+ attr_reader :syntax_highlighter
43
+
44
+ before_body do
45
+ @syntax_highlighter = SyntaxHighlighter.new(language: language, theme: theme)
46
+ @font_default = {family: font_family, size: font_size, weight: :medium, italic: :normal, stretch: :normal}
47
+ @font_italic = @font_default.merge(italic: :italic)
48
+ end
47
49
 
48
50
  body {
49
51
  area {
50
52
  rectangle(0, 0, 8000, 8000) {
51
53
  fill :white
52
54
  }
53
- text {
54
- default_font family: OS.mac? ? 'Consolas' : 'Courier', size: 13, weight: :medium, italic: :normal, stretch: :normal
55
+ text(padding, padding) {
56
+ default_font @font_default
55
57
 
56
- syntax_highlighting(code).each do |token|
57
- style_data = Rouge::Theme.find(theme).new.style_for(token[:token_type])
58
-
59
- string(token[:token_text]) {
60
- color style_data[:fg] || :black
61
- background style_data[:bg] || :white
58
+ syntax_highlighter.syntax_highlighting(code).each do |token|
59
+ token_text = token[:token_text].start_with?("\n") ? " #{token[:token_text]}" : token[:token_text]
60
+
61
+ string(token_text) {
62
+ font @font_italic if token[:token_style][:italic]
63
+ color token[:token_style][:fg] || :black
64
+ background token[:token_style][:bg] || :white
62
65
  }
63
66
  end
64
67
  }
65
68
  }
66
69
  }
67
-
68
- def lexer
69
- require 'rouge'
70
- require 'glimmer-dsl-libui/ext/rouge/theme/glimmer'
71
- # TODO Try to use Rouge::Lexer.find_fancy('guess', code) in the future to guess the language or otherwise detect it from file extension
72
- @lexer ||= Rouge::Lexer.find_fancy(language)
73
- @lexer ||= Rouge::Lexer.find_fancy('ruby') # default to Ruby if no lexer is found
74
- end
75
-
76
- def syntax_highlighting(text)
77
- return [] if text.to_s.strip.empty?
78
- @syntax_highlighting ||= {}
79
- unless @syntax_highlighting.keys.include?(text)
80
- lex = lexer.lex(text).to_a
81
- text_size = 0
82
- @syntax_highlighting[text] = lex.map do |pair|
83
- {token_type: pair.first, token_text: pair.last}
84
- end.each do |hash|
85
- hash[:token_index] = text_size
86
- text_size += hash[:token_text].size
87
- end
88
- end
89
- @syntax_highlighting[text]
90
- end
91
70
  end
92
71
  end
93
72
  end
@@ -0,0 +1,300 @@
1
+ # Copyright (c) 2021-2024 Andy Maleh
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
7
+ # distribute, sublicense, and/or sell copies of the Software, and to
8
+ # permit persons to whom the Software is furnished to do so, subject to
9
+ # the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+ require 'os'
23
+
24
+ require 'glimmer/libui/custom_control'
25
+ require 'glimmer/libui/syntax_highlighter'
26
+
27
+ module Glimmer
28
+ module LibUI
29
+ module CustomControl
30
+ class CodeEntry
31
+ include Glimmer::LibUI::CustomControl
32
+
33
+ REGEX_COLOR_HEX6 = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/
34
+ # TODO vary shortcut key by OS (CMD for Mac, CTRL elsewhere)
35
+
36
+ option :language, default: 'ruby'
37
+ option :theme, default: 'glimmer'
38
+ option :code
39
+ option :padding, default: 10
40
+ option :caret_blinking_delay_in_seconds, default: 0.5
41
+ option :font_family, default: OS.mac? ? 'Courier New' : 'Courier'
42
+ option :font_size, default: 14
43
+ # TODO consider offering the option to autosave to a file upon changes
44
+
45
+ attr_reader :syntax_highlighter, :line, :position
46
+
47
+ before_body do
48
+ @syntax_highlighter = SyntaxHighlighter.new(language: language, theme: theme)
49
+ @font_default = {family: font_family, size: font_size, weight: :medium, italic: :normal, stretch: :normal}
50
+ @font_italic = @font_default.merge(italic: :italic)
51
+ @line = 0
52
+ @position = 5
53
+ @draw_caret = false
54
+ @multiplier_position = 0.6
55
+ @multiplier_line = 1.2
56
+ end
57
+
58
+ after_body do
59
+ LibUI.timer(caret_blinking_delay_in_seconds/2.0) do
60
+ body_root.redraw
61
+ end
62
+ end
63
+
64
+ body {
65
+ scrolling_area(1, 1) { |code_entry_area|
66
+ on_draw do
67
+ # TODO need to determine the scrolling area width and height from the text extent once supported in the future
68
+ # TODO only reset size when a new line has been added
69
+ area_width = longest_line_size * font_size*@multiplier_position
70
+ area_height = line_count * font_size*@multiplier_line
71
+ code_entry_area.set_size(area_width, area_height)
72
+
73
+ rectangle(0, 0, area_width, area_height) {
74
+ fill :white
75
+ }
76
+
77
+ code_layer
78
+
79
+ caret_layer if @draw_caret
80
+
81
+ if @blinking_time.nil? || (Time.now - @blinking_time > caret_blinking_delay_in_seconds)
82
+ @blinking_time = Time.now
83
+ @draw_caret = !@draw_caret
84
+ end
85
+ end
86
+
87
+ on_mouse_down do |mouse_event|
88
+ # once text extent calculation via libui is supported, consider the idea of splitting
89
+ # text by single characters to use every character extent in determining mouse location
90
+ # or not splitting but using the extent of one character to determine mouse location
91
+ @position = (mouse_event[:x] - padding) / (font_size*@multiplier_position)
92
+ @line = (mouse_event[:y] - padding) / (font_size*@multiplier_line)
93
+ @line = [@line, code.lines.length - 1].min
94
+ @position = [@position, current_code_line_max_position].min
95
+ body_root.redraw
96
+ end
97
+ # TODO mouse click based text selection
98
+ # TODO keyboar based text selection
99
+
100
+ on_key_down do |key_event|
101
+ # TODO consider delegating some of the logic below to a model
102
+ handled = true # assume it is handled for all cases except the else clause below
103
+ case key_event
104
+ in modifiers: [], ext_key: :left
105
+ if @position == 0
106
+ if @line > 0
107
+ new_position = code.lines[line - 1].length - 1
108
+ @line = [@line - 1, 0].max
109
+ @position = new_position
110
+ end
111
+ else
112
+ @position = [@position - 1, 0].max
113
+ end
114
+ in modifiers: [], ext_key: :right
115
+ if @position == current_code_line_max_position
116
+ if @line < code.lines.size - 1
117
+ @line += 1
118
+ @position = 0
119
+ end
120
+ else
121
+ @position += 1
122
+ end
123
+ in modifiers: [], ext_key: :up
124
+ # TODO scroll view when going down or up or paging or going home / end
125
+ @line = [@line - 1, 0].max
126
+ if @max_position
127
+ @position = @max_position
128
+ @max_position = nil
129
+ end
130
+ in modifiers: [], ext_key: :down
131
+ @line += 1
132
+ if @max_position
133
+ @position = @max_position
134
+ @max_position = nil
135
+ end
136
+ in modifiers: [], ext_key: :page_up
137
+ @line = [@line - 15, 0].max
138
+ if @max_position
139
+ @position = @max_position
140
+ @max_position = nil
141
+ end
142
+ in modifiers: [], ext_key: :page_down
143
+ @line += 15
144
+ if @max_position
145
+ @position = @max_position
146
+ @max_position = nil
147
+ end
148
+ in modifiers: [], ext_key: :home
149
+ @line = 0
150
+ @position = 0
151
+ in modifiers: [], ext_key: :end
152
+ @line = code.lines.size - 1
153
+ @position = current_code_line_max_position
154
+ in ext_key: :delete
155
+ code.slice!(caret_index)
156
+ in key: "\n"
157
+ code.insert(caret_index, "\n")
158
+ @line += 1
159
+ @position = 0
160
+ # TODO indent upon hitting enter
161
+ in key: "\b"
162
+ if @position == 0
163
+ if @line > 0
164
+ new_position = code.lines[line - 1].length - 1
165
+ code.slice!(caret_index - 1)
166
+ @line = [@line - 1, 0].max
167
+ @position = new_position
168
+ end
169
+ else
170
+ @position = [@position - 1, 0].max
171
+ code.slice!(caret_index)
172
+ end
173
+ in key: "\t"
174
+ code.insert(caret_index, ' ')
175
+ @position += 2
176
+ in modifiers: [:control], key: 'a'
177
+ @position = 0
178
+ in modifiers: [:command], ext_key: :left
179
+ @position = 0
180
+ in modifiers: [:control], key: 'e'
181
+ @position = current_code_line_max_position
182
+ in modifiers: [:command], ext_key: :right
183
+ @position = current_code_line_max_position
184
+ in modifiers: [:shift], key_code: 48
185
+ code.insert(caret_index, ')')
186
+ @position += 1
187
+ in modifiers: [:alt], ext_key: :right
188
+ if @position == current_code_line_max_position
189
+ if @line < code.lines.size - 1
190
+ @line += 1
191
+ @position = 0
192
+ end
193
+ else
194
+ new_caret_index = caret_index
195
+ new_caret_index += 1 while code[new_caret_index + 1]&.match(/[^a-zA-Z]/)
196
+ new_caret_index += 1 until code[new_caret_index + 1].nil? || code[new_caret_index + 1].match(/[^a-zA-Z]/)
197
+ @position += new_caret_index + 1 - caret_index
198
+ end
199
+ in modifiers: [:alt], ext_key: :left
200
+ if @position == 0
201
+ if @line > 0
202
+ new_position = code.lines[line - 1].length - 1
203
+ @line = [@line - 1, 0].max
204
+ @position = new_position
205
+ end
206
+ else
207
+ new_caret_index = caret_index
208
+ new_caret_index -= 1 while code[new_caret_index - 1]&.match(/[^a-zA-Z]/)
209
+ new_caret_index -= 1 until code[new_caret_index + 1].nil? || code[new_caret_index - 1].match(/[^a-zA-Z]/)
210
+ @position -= caret_index - new_caret_index
211
+ @position = [@position, 0].max
212
+ end
213
+ in modifier: nil, modifiers: []
214
+ code.insert(caret_index, key_event[:key])
215
+ @position += 1
216
+ in modifier: nil, modifiers: [:shift]
217
+ character = key_event[:key] || key_event[:key_code].chr.capitalize
218
+ code.insert(caret_index, character)
219
+ @position += 1
220
+ # TODO CMD Z (undo)
221
+ # TODO CMD SHIFT Z (redo)
222
+ # TODO CMD + [ (outdent)
223
+ # TODO CMD + ] (indent)
224
+ # TODO CMD + down (move line down)
225
+ # TODO CMD + up (move line up)
226
+ # TODO CMD + D (duplicate)
227
+ else
228
+ handled = false
229
+ end
230
+ @line = [@line, code.lines.length - 1].min
231
+ @line = [@line, 0].max
232
+ new_position = [@position, current_code_line_max_position].min
233
+ if new_position != @position
234
+ @max_position = @position
235
+ @position = new_position
236
+ end
237
+ @draw_caret = true
238
+ body_root.redraw
239
+ handled
240
+ end
241
+ }
242
+ }
243
+
244
+ def code_layer
245
+ text(padding, padding) {
246
+ default_font @font_default
247
+
248
+ syntax_highlighter.syntax_highlighting(code).each do |token|
249
+ token_text = token[:token_text].start_with?("\n") ? " #{token[:token_text]}" : token[:token_text]
250
+
251
+ string(token_text) {
252
+ font @font_italic if token[:token_style][:italic]
253
+ color token[:token_style][:fg] || :black
254
+ background token[:token_style][:bg] || :white
255
+ }
256
+ end
257
+ }
258
+ end
259
+
260
+ def caret_layer
261
+ # TODO adjust padding offset based on text extent
262
+ text(padding - 4, padding) {
263
+ default_font @font_default
264
+
265
+ # TODO make caret blink
266
+ string(caret_text) {
267
+ color :black
268
+ background [0, 0, 0, 0]
269
+ }
270
+ }
271
+ end
272
+
273
+ def caret_text
274
+ # TODO replace | with a real caret (see if there is an emoji or special character for it)
275
+ ("\n" * @line) + (' ' * @position) + '|'
276
+ end
277
+
278
+ def caret_index
279
+ code.lines[0..line].join.length - (current_code_line.length - @position)
280
+ end
281
+
282
+ def current_code_line
283
+ code.lines[@line]
284
+ end
285
+
286
+ def current_code_line_max_position
287
+ (current_code_line && current_code_line.length > 0) ? (current_code_line.length - 1) : 0
288
+ end
289
+
290
+ def longest_line_size
291
+ code&.lines&.map(&:size)&.max || 1
292
+ end
293
+
294
+ def line_count
295
+ code&.lines&.size || 1
296
+ end
297
+ end
298
+ end
299
+ end
300
+ end
@@ -43,7 +43,7 @@ module Glimmer
43
43
  # and updates view property accordingly
44
44
  def data_bind_read(property, model_binding)
45
45
  model_attribute_observer = Glimmer::DataBinding::Observer.proc do
46
- new_value = model_binding.evaluate_property
46
+ new_value = model_binding.evaluate_property # TODO performance might be worse for not using block value instead (double double call on evaluate_property)
47
47
  send("#{property}=", new_value) unless send(property) == new_value
48
48
  end
49
49
  observer_registration = model_attribute_observer.observe(model_binding, attribute_writer_type: [:attribute=, :set_attribute])
@@ -0,0 +1,47 @@
1
+ class SyntaxHighlighter
2
+ class << self
3
+ def languages
4
+ require 'rouge'
5
+ Rouge::Lexer.all.map {|lexer| lexer.tag}.sort
6
+ end
7
+
8
+ def lexers
9
+ require 'rouge'
10
+ Rouge::Lexer.all.sort_by(&:title)
11
+ end
12
+ end
13
+
14
+ attr_reader :language, :theme
15
+
16
+ def initialize(language:, theme:)
17
+ @language = language
18
+ @theme = theme
19
+ end
20
+
21
+ def lexer
22
+ require 'rouge'
23
+ require 'glimmer-dsl-libui/ext/rouge/theme/glimmer'
24
+ # TODO Try to use Rouge::Lexer.find_fancy('guess', code) in the future to guess the language or otherwise detect it from file extension
25
+ @lexer ||= Rouge::Lexer.find_fancy(language)
26
+ @lexer ||= Rouge::Lexer.find_fancy('ruby') # default to Ruby if no lexer is found
27
+ end
28
+
29
+ def syntax_highlighting(text)
30
+ return [] if text.to_s.strip.empty?
31
+ @syntax_highlighting ||= {} # TODO consider memoizing/remembering the last value only to avoid wasting memory
32
+ unless @syntax_highlighting.keys.include?(text)
33
+ lex = lexer.lex(text).to_a
34
+ text_size = 0
35
+ @syntax_highlighting[text] = lex.map do |pair|
36
+ token_type = pair.first
37
+ token_text = pair.last
38
+ token_style = Rouge::Theme.find(theme).new.style_for(token_type)
39
+ {token_type: token_type, token_text: token_text, token_style: token_style}
40
+ end.each do |hash|
41
+ hash[:token_index] = text_size # TODO do we really need this? Also, consider renaming to something like token_character_index to clarify it is not the index of the token itself yet its first character
42
+ text_size += hash[:token_text].size
43
+ end
44
+ end
45
+ @syntax_highlighting[text]
46
+ end
47
+ end
data/lib/glimmer/libui.rb CHANGED
@@ -199,6 +199,13 @@ module Glimmer
199
199
  Color::RGB.constants.reject {|c| c.to_s.upcase == c.to_s}.map(&:to_s).map(&:underscore).map(&:to_sym)
200
200
  end
201
201
 
202
+ # Returns OS shortcut key, meaning the key used with most shorcuts,
203
+ # like :command on the Mac (used in CMD+S for save)
204
+ # or :control on Windows and Linux (used in CONTROL+S for save)
205
+ def os_shortcut_key
206
+ @os_shortcut_key ||= OS.mac? ? :command : :ctrl
207
+ end
208
+
202
209
  # Queues block to execute at the nearest opportunity possible on the main GUI event loop
203
210
  def queue_main(&block)
204
211
  closure = fiddle_closure_block_caller(4, [0]) do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: glimmer-dsl-libui
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.1
4
+ version: 0.12.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Maleh
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-06-24 00:00:00.000000000 Z
11
+ date: 2024-07-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: glimmer
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 2.7.4
19
+ version: 2.7.9
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 2.7.4
26
+ version: 2.7.9
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: perfect-shape
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -58,7 +58,7 @@ dependencies:
58
58
  requirements:
59
59
  - - ">="
60
60
  - !ruby/object:Gem::Version
61
- version: 1.0.0
61
+ version: 1.0.1
62
62
  - - "<"
63
63
  - !ruby/object:Gem::Version
64
64
  version: 2.0.0
@@ -68,7 +68,7 @@ dependencies:
68
68
  requirements:
69
69
  - - ">="
70
70
  - !ruby/object:Gem::Version
71
- version: 1.0.0
71
+ version: 1.0.1
72
72
  - - "<"
73
73
  - !ruby/object:Gem::Version
74
74
  version: 2.0.0
@@ -388,6 +388,7 @@ files:
388
388
  - examples/basic_button.rb
389
389
  - examples/basic_child_window.rb
390
390
  - examples/basic_code_area.rb
391
+ - examples/basic_code_entry.rb
391
392
  - examples/basic_composite_shape.rb
392
393
  - examples/basic_custom_shape.rb
393
394
  - examples/basic_draw_text.rb
@@ -592,6 +593,7 @@ files:
592
593
  - lib/glimmer/libui/control_proxy/window_proxy.rb
593
594
  - lib/glimmer/libui/custom_control.rb
594
595
  - lib/glimmer/libui/custom_control/code_area.rb
596
+ - lib/glimmer/libui/custom_control/code_entry.rb
595
597
  - lib/glimmer/libui/custom_control/refined_table.rb
596
598
  - lib/glimmer/libui/custom_shape.rb
597
599
  - lib/glimmer/libui/custom_window.rb
@@ -611,6 +613,7 @@ files:
611
613
  - lib/glimmer/libui/shape/polyline.rb
612
614
  - lib/glimmer/libui/shape/rectangle.rb
613
615
  - lib/glimmer/libui/shape/square.rb
616
+ - lib/glimmer/libui/syntax_highlighter.rb
614
617
  - lib/glimmer/proc_tracker.rb
615
618
  - lib/glimmer/rake_task.rb
616
619
  - lib/glimmer/rake_task/list.rb