glimmer-dsl-libui 0.12.0 → 0.12.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -0
- data/README.md +13 -3
- data/VERSION +1 -1
- data/examples/basic_code_area.rb +0 -3
- data/examples/basic_code_entry.rb +30 -0
- data/glimmer-dsl-libui.gemspec +0 -0
- data/lib/glimmer/libui/control_proxy/tab_item_proxy.rb +12 -4
- data/lib/glimmer/libui/control_proxy/text_proxy.rb +12 -12
- data/lib/glimmer/libui/control_proxy.rb +3 -0
- data/lib/glimmer/libui/custom_control/code_area.rb +23 -44
- data/lib/glimmer/libui/custom_control/code_entry.rb +300 -0
- data/lib/glimmer/libui/data_bindable.rb +1 -1
- data/lib/glimmer/libui/syntax_highlighter.rb +47 -0
- data/lib/glimmer/libui.rb +7 -0
- data/lib/glimmer-dsl-libui.rb +10 -1
- metadata +9 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 467e3450aca13a338d5d835aa79c7b483cd9475866d6e6f08e2b130386107d1a
|
4
|
+
data.tar.gz: 81674d7b0b95d973b5286c1c8cc187e747412e02a6a0f7bffc7d235b499186c6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ada3fc2a2db26d4660876010f1b8f0cdc08797baa0b0e63e5a044e050dc9cf400f6318dc7e09328137b755c861695c1a74901ec91be2add91433c1d487ba1438
|
7
|
+
data.tar.gz: f1aee5106ee4500c46970de47136da231c4e096a213f65095cb2640e8b978672100ef036ebb8a24fbd65137ec222f84a345832c6fe69bc87066599496658c8b6
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,21 @@
|
|
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
|
+
|
15
|
+
## 0.12.1
|
16
|
+
|
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.
|
18
|
+
|
3
19
|
## 0.12.0
|
4
20
|
|
5
21
|
- Custom Control Component Slots (containers that could accept content within different parts of a Custom Component)
|
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
|
+
# [<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.
|
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):
|
@@ -492,6 +492,8 @@ If you are new to [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-
|
|
492
492
|
|
493
493
|
If you encounter any issues with the documentation, get stuck with code you do not understand, or notice some out-of-date information, you may contact the project maintainers on the [Glimmer Gitter Chat](https://app.gitter.im/#/room/#AndyObtiva_glimmer:gitter.im). Also, this could be your opportunity to be a good steward of Open-Source Software by contributing a documentation fix in a GitHub Pull Request or reporting a GitHub Issue at least.
|
494
494
|
|
495
|
+
For integration with a Relational Database (SQL) via ActiveRecord, you may refer to the blog post that was written about [using ActiveRecord with SQLite DB and Glimmer DSL for SWT](https://andymaleh.blogspot.com/2022/06/using-activerecord-with-sqlite-db-in.html) (altering to fit CRuby and Glimmer DSL for LibUI). Also, [@chip](https://github.com/chip) created a prototype Git repo for starting a Glimmer DSL for LibUI project with SQLite DB and ActiveRecord: https://github.com/chip/glimmer_dsl_with_active_record
|
496
|
+
|
495
497
|
### Experimentation Usage
|
496
498
|
|
497
499
|
For experimenting and learning, add `include Glimmer` into the top-level main object and start using the Glimmer GUI DSL directly.
|
@@ -3320,7 +3322,7 @@ ClassBasedCustomControls.launch
|
|
3320
3322
|
|
3321
3323
|
Component can have Component Slots inside layout container controls: `vertical_box`, `horizontal_box`, `form`, and `grid`.
|
3322
3324
|
|
3323
|
-
Simply
|
3325
|
+
Simply designate a layout container control as a Component Slot inside a Custom Control class body block by passing it a `slot: slot_name` option (in the example below, we have a `:header` slot and a `:footer` slot):
|
3324
3326
|
|
3325
3327
|
```ruby
|
3326
3328
|
body {
|
@@ -4499,6 +4501,14 @@ https://rubygems.org/gems/adamantite
|
|
4499
4501
|
|
4500
4502
|
![Adamantite password manager](/images/glimmer-dsl-libui-mac-application-adamantite.png)
|
4501
4503
|
|
4504
|
+
### Ruby Go
|
4505
|
+
|
4506
|
+
A simple app for playing Go games with a friend, built by [Tim Standen](https://github.com/timbot1789).
|
4507
|
+
|
4508
|
+
https://github.com/timbot1789/rubygo
|
4509
|
+
|
4510
|
+
![Ruby Go](/images/glimmer-dsl-libui-mac-application-ruby-go.png)
|
4511
|
+
|
4502
4512
|
## Design Principles
|
4503
4513
|
|
4504
4514
|
- The Ruby Way (including TIMTOWTDI: There Is More Than One Way To Do It)
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.12.
|
1
|
+
0.12.2
|
data/examples/basic_code_area.rb
CHANGED
@@ -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
|
data/glimmer-dsl-libui.gemspec
CHANGED
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
|
-
@
|
64
|
-
@
|
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
|
55
|
+
text(padding, padding) {
|
56
|
+
default_font @font_default
|
55
57
|
|
56
|
-
syntax_highlighting(code).each do |token|
|
57
|
-
|
58
|
-
|
59
|
-
string(
|
60
|
-
|
61
|
-
|
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
|
data/lib/glimmer-dsl-libui.rb
CHANGED
@@ -21,8 +21,17 @@
|
|
21
21
|
|
22
22
|
$LOAD_PATH.unshift(File.expand_path('..', __FILE__))
|
23
23
|
|
24
|
-
#
|
24
|
+
# concurrent-ruby gem ensures glimmer relies on Concurrent data-structure classes
|
25
|
+
# Load if available only to ensure loading at the right time before loading glimmer
|
26
|
+
begin
|
27
|
+
require 'concurrent/array'
|
28
|
+
require 'concurrent/hash'
|
29
|
+
require 'concurrent/set'
|
30
|
+
rescue LoadError
|
31
|
+
# No Op
|
32
|
+
end
|
25
33
|
require 'glimmer'
|
34
|
+
# External requires
|
26
35
|
# require 'logging'
|
27
36
|
require 'puts_debuggerer' if (ENV['PD'] || ENV['pd']).to_s.downcase == 'true'
|
28
37
|
# require 'super_module'
|
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.
|
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-
|
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.
|
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.
|
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.
|
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.
|
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
|