glimmer-dsl-libui 0.12.1 → 0.12.2

Sign up to get free protection for your applications and to get access to all the features.
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