konpeito 0.1.1 → 0.1.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 +4 -4
- data/CHANGELOG.md +14 -0
- data/README.md +79 -22
- data/lib/konpeito/codegen/jvm_generator.rb +26 -2
- data/lib/konpeito/type_checker/hm_inferrer.rb +22 -0
- data/lib/konpeito/type_checker/unification.rb +35 -0
- data/lib/konpeito/ui/column.rb +45 -10
- data/lib/konpeito/ui/core.rb +270 -24
- data/lib/konpeito/ui/row.rb +54 -12
- data/lib/konpeito/ui/theme.rb +5 -5
- data/lib/konpeito/ui/widgets/button.rb +3 -3
- data/lib/konpeito/ui/widgets/checkbox.rb +0 -2
- data/lib/konpeito/ui/widgets/container.rb +18 -0
- data/lib/konpeito/ui/widgets/image.rb +0 -2
- data/lib/konpeito/ui/widgets/input.rb +0 -2
- data/lib/konpeito/ui/widgets/markdown.rb +0 -2
- data/lib/konpeito/ui/widgets/multiline_text.rb +0 -2
- data/lib/konpeito/ui/widgets/net_image.rb +0 -2
- data/lib/konpeito/ui/widgets/radio_buttons.rb +0 -2
- data/lib/konpeito/ui/widgets/switch.rb +0 -2
- data/lib/konpeito/ui/widgets/text.rb +11 -3
- data/lib/konpeito/ui/widgets/tree.rb +6 -0
- data/lib/konpeito/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 53a3bbb24175ed554f9f560c38e6f8138206da3fb6385f87358e9dfa3eed4f10
|
|
4
|
+
data.tar.gz: 80026dca6352dcb9effa9e7bac8e7ebc58e517ae84cb1f15627935fbb3d5fee4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4233ae433c5730c64da97806275ca0300d80f77e5af8c338cf2185f54205d69b99091447a097506b5d2944dac437396a8a4e04ae9a97a90df60f8a93d60e730d
|
|
7
|
+
data.tar.gz: 911c32b9ac29989060b08fe6fc352074a02038ae20f8b516723e56a263bf602b0ee70206cf41a3bbe682044ab4c7511552fa255ef9d7b137da952dcea271f860
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,19 @@ All notable changes to Konpeito will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.1.2] - 2026-02-17
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- Castella UI: include padding and respect FIXED sizes in Column/Row measure
|
|
12
|
+
- Castella UI: use lighter accent variant for normal button hover across all themes
|
|
13
|
+
- JVM backend: preserve subclass type through inherited self-returning method chains
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- Documentation: reorganize Castella UI section in README with DSL / Reactive State / Style Composition subheadings
|
|
17
|
+
- Documentation: add Style Composition example and screenshot to README
|
|
18
|
+
- Documentation: remove filler spacers from counter demos (EXPANDING default makes them unnecessary)
|
|
19
|
+
- Documentation: update counter screenshot
|
|
20
|
+
|
|
8
21
|
## [0.1.1] - 2026-02-16
|
|
9
22
|
|
|
10
23
|
### Fixed
|
|
@@ -86,5 +99,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
86
99
|
- `%a{extern}` - external C struct wrappers
|
|
87
100
|
- `%a{simd}` - SIMD vectorization
|
|
88
101
|
|
|
102
|
+
[0.1.2]: https://github.com/i2y/konpeito/compare/v0.1.1...v0.1.2
|
|
89
103
|
[0.1.1]: https://github.com/i2y/konpeito/compare/v0.1.0...v0.1.1
|
|
90
104
|
[0.1.0]: https://github.com/i2y/konpeito/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -135,43 +135,100 @@ The JVM backend supports seamless Java interop — call Java libraries directly
|
|
|
135
135
|
|
|
136
136
|
## Castella UI
|
|
137
137
|
|
|
138
|
-
|
|
138
|
+
A reactive GUI framework for the JVM backend, powered by [JWM](https://github.com/nicenote/jwm) + [Skija](https://github.com/nicenote/skija).
|
|
139
|
+
|
|
140
|
+
### DSL
|
|
141
|
+
|
|
142
|
+
Ruby's block syntax becomes a UI DSL — `column`, `row`, `text`, `button` etc. nest naturally with keyword arguments. A plain Ruby method is a reusable component.
|
|
143
|
+
|
|
144
|
+
<p align="center">
|
|
145
|
+
<img src="docs/screenshots/dashboard.png" alt="Analytics Dashboard" width="720" />
|
|
146
|
+
</p>
|
|
139
147
|
|
|
140
148
|
```ruby
|
|
141
|
-
|
|
149
|
+
def view
|
|
150
|
+
column(padding: 20.0, spacing: 16.0) {
|
|
151
|
+
row(spacing: 12.0) {
|
|
152
|
+
text("Analytics Dashboard", font_size: 26.0, bold: true)
|
|
153
|
+
spacer
|
|
154
|
+
button("Refresh", width: 90.0) {}
|
|
155
|
+
}.fixed_height(40.0)
|
|
156
|
+
|
|
157
|
+
# KPI Cards — extract a method, and it's a reusable component
|
|
158
|
+
row(spacing: 12.0) {
|
|
159
|
+
kpi_card("Revenue", "$48,250", "+12.5%", $theme.accent)
|
|
160
|
+
kpi_card("Users", "3,842", "+8.1%", $theme.success)
|
|
161
|
+
kpi_card("Orders", "1,205", "-2.3%", $theme.error)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
# Charts, tables, and layouts compose with blocks
|
|
165
|
+
row(spacing: 12.0) {
|
|
166
|
+
column(expanding_width: true, bg_color: $theme.bg_primary, border_radius: 10.0, padding: 14.0) {
|
|
167
|
+
bar_chart(labels, data, ["Revenue", "Costs"]).title("Monthly Overview").fixed_height(220.0)
|
|
168
|
+
}
|
|
169
|
+
column(expanding_width: true, bg_color: $theme.bg_primary, border_radius: 10.0, padding: 14.0) {
|
|
170
|
+
data_table(headers, widths, rows).fixed_height(200.0)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# A Ruby method is a reusable component
|
|
177
|
+
def kpi_card(label, value, change, color)
|
|
178
|
+
column(spacing: 6.0, bg_color: $theme.bg_primary, border_radius: 10.0, padding: 16.0, expanding_width: true) {
|
|
179
|
+
text(label, font_size: 12.0, color: $theme.text_secondary)
|
|
180
|
+
text(value, font_size: 24.0, bold: true)
|
|
181
|
+
text(change, font_size: 13.0, color: color)
|
|
182
|
+
}
|
|
183
|
+
end
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Reactive State
|
|
187
|
+
|
|
188
|
+
`state(0)` creates an observable value, and the UI re-renders automatically when it changes:
|
|
189
|
+
|
|
190
|
+
<p align="center">
|
|
191
|
+
<img src="docs/screenshots/counter.png" alt="Counter App" width="320" />
|
|
192
|
+
</p>
|
|
193
|
+
|
|
194
|
+
```ruby
|
|
195
|
+
class Counter < Component
|
|
142
196
|
def initialize
|
|
143
197
|
super
|
|
144
198
|
@count = state(0)
|
|
145
199
|
end
|
|
146
200
|
|
|
147
201
|
def view
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
)
|
|
202
|
+
column(padding: 16.0, spacing: 8.0) {
|
|
203
|
+
text "Count: #{@count}", font_size: 32.0, align: :center
|
|
204
|
+
row(spacing: 8.0) {
|
|
205
|
+
button(" - ") { @count -= 1 }
|
|
206
|
+
button(" + ") { @count += 1 }
|
|
207
|
+
}
|
|
208
|
+
}
|
|
156
209
|
end
|
|
157
210
|
end
|
|
158
211
|
```
|
|
159
212
|
|
|
160
|
-
|
|
213
|
+
An OOP-style API (`Column(...)`, `Row(...)`) is also available. Available widgets: `Text`, `Button`, `TextInput`, `MultilineText`, `Column`, `Row`, `Image`, `Checkbox`, `Slider`, `ProgressBar`, `Tabs`, `DataTable`, `TreeView`, `BarChart`, `LineChart`, `PieChart`, `Markdown`, and more.
|
|
214
|
+
|
|
215
|
+
### Style Composition
|
|
216
|
+
|
|
217
|
+
Styles are first-class objects — store them in variables and compose with `+`:
|
|
218
|
+
|
|
219
|
+
<p align="center">
|
|
220
|
+
<img src="docs/screenshots/style_composition.png" alt="Style Composition" width="480" />
|
|
221
|
+
</p>
|
|
161
222
|
|
|
162
223
|
```ruby
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
text "Counter: #{@count}", font_size: 24.0, color: 0xFFC0CAF5
|
|
166
|
-
row(spacing: 8.0) do
|
|
167
|
-
button("-", width: 60.0) { @count -= 1 }
|
|
168
|
-
button("+", width: 60.0) { @count += 1 }
|
|
169
|
-
end
|
|
170
|
-
end
|
|
171
|
-
end
|
|
172
|
-
```
|
|
224
|
+
card = Style.new.bg_color($theme.bg_primary).border_radius(10.0).padding(16.0)
|
|
225
|
+
green_card = card + Style.new.border_color($theme.success)
|
|
173
226
|
|
|
174
|
-
|
|
227
|
+
column(spacing: 12.0) {
|
|
228
|
+
container(card) { text "Default card" }
|
|
229
|
+
container(green_card) { text "Green border variant" }
|
|
230
|
+
}
|
|
231
|
+
```
|
|
175
232
|
|
|
176
233
|
## Performance
|
|
177
234
|
|
|
@@ -3148,6 +3148,22 @@ module Konpeito
|
|
|
3148
3148
|
ensure_slot(result_var, ret_type)
|
|
3149
3149
|
instructions << store_instruction(result_var, ret_type)
|
|
3150
3150
|
@variable_types[result_var] = ret_type
|
|
3151
|
+
|
|
3152
|
+
# Propagate class type so subsequent method chains resolve the receiver.
|
|
3153
|
+
# Without this, `Button("Animate!").kind(1)` fails because the receiver
|
|
3154
|
+
# class of `.kind(1)` is unknown after the static call to `Button()`.
|
|
3155
|
+
if ret_type.is_a?(Symbol) && ret_type.to_s.start_with?("class:")
|
|
3156
|
+
cls = ret_type.to_s.sub("class:", "")
|
|
3157
|
+
@variable_class_types[result_var] = cls if @class_info.key?(cls)
|
|
3158
|
+
end
|
|
3159
|
+
# Also check HM-inferred type from the call instruction
|
|
3160
|
+
if !@variable_class_types[result_var] && inst.respond_to?(:type) && inst.type
|
|
3161
|
+
call_type = inst.type
|
|
3162
|
+
call_type = call_type.prune if call_type.respond_to?(:prune)
|
|
3163
|
+
if call_type.is_a?(TypeChecker::Types::ClassInstance) && @class_info.key?(call_type.name.to_s)
|
|
3164
|
+
@variable_class_types[result_var] = call_type.name.to_s
|
|
3165
|
+
end
|
|
3166
|
+
end
|
|
3151
3167
|
end
|
|
3152
3168
|
end
|
|
3153
3169
|
|
|
@@ -5114,8 +5130,16 @@ module Konpeito
|
|
|
5114
5130
|
# checkcast when return type is a user class (descriptor returns Object but actual is specific class)
|
|
5115
5131
|
if ret_type == :value
|
|
5116
5132
|
rbs_class = resolve_rbs_return_class_name(owner_class, method_name)
|
|
5117
|
-
#
|
|
5118
|
-
|
|
5133
|
+
# When function returns self (SelfRef), preserve the receiver's actual class type.
|
|
5134
|
+
# RBS annotations declare the defining class (e.g., Widget#fixed_height → Widget)
|
|
5135
|
+
# but `self` at runtime is the receiver's class (e.g., Button). Without this,
|
|
5136
|
+
# method chains like Button(...).fixed_height(36.0).on_click { ... } break because
|
|
5137
|
+
# the receiver type narrows to Widget and on_click (defined on Button) is not found.
|
|
5138
|
+
if inferred_return_class
|
|
5139
|
+
rbs_class = inferred_return_class
|
|
5140
|
+
else
|
|
5141
|
+
rbs_class ||= inferred_return_class
|
|
5142
|
+
end
|
|
5119
5143
|
if rbs_class && @class_info.key?(rbs_class)
|
|
5120
5144
|
instructions << { "op" => "checkcast", "type" => @class_info[rbs_class][:jvm_name] }
|
|
5121
5145
|
@variable_class_types[result_var] = rbs_class
|
|
@@ -1429,6 +1429,10 @@ module Konpeito
|
|
|
1429
1429
|
# Score how well an argument type matches an RBS parameter type
|
|
1430
1430
|
def overload_match_score(arg_type, rbs_param_type)
|
|
1431
1431
|
case rbs_param_type
|
|
1432
|
+
when RBS::Types::Alias
|
|
1433
|
+
# Resolve common aliases: ::int -> Integer, ::string -> String, etc.
|
|
1434
|
+
resolved = resolve_rbs_alias(rbs_param_type)
|
|
1435
|
+
return overload_match_score(arg_type, resolved) if resolved
|
|
1432
1436
|
when RBS::Types::ClassInstance
|
|
1433
1437
|
param_name = rbs_param_type.name.name
|
|
1434
1438
|
if arg_type.is_a?(Types::ClassInstance)
|
|
@@ -1444,6 +1448,24 @@ module Konpeito
|
|
|
1444
1448
|
0
|
|
1445
1449
|
end
|
|
1446
1450
|
|
|
1451
|
+
# Resolve RBS type aliases to their underlying ClassInstance types
|
|
1452
|
+
def resolve_rbs_alias(alias_type)
|
|
1453
|
+
alias_name = alias_type.name.name
|
|
1454
|
+
class_name = case alias_name
|
|
1455
|
+
when :int then :Integer
|
|
1456
|
+
when :string then :String
|
|
1457
|
+
when :float then :Float
|
|
1458
|
+
when :bool then :Bool
|
|
1459
|
+
when :boolish then :Bool
|
|
1460
|
+
end
|
|
1461
|
+
return nil unless class_name
|
|
1462
|
+
RBS::Types::ClassInstance.new(
|
|
1463
|
+
name: RBS::TypeName.new(name: class_name, namespace: RBS::Namespace.root),
|
|
1464
|
+
args: [],
|
|
1465
|
+
location: alias_type.location
|
|
1466
|
+
)
|
|
1467
|
+
end
|
|
1468
|
+
|
|
1447
1469
|
def numeric_compatible?(arg_name, param_name)
|
|
1448
1470
|
numerics = %i[Integer Float Rational Complex Numeric]
|
|
1449
1471
|
numerics.include?(arg_name) && numerics.include?(param_name)
|
|
@@ -186,6 +186,9 @@ module Konpeito
|
|
|
186
186
|
elsif singleton_value_compatible?(t1, t2)
|
|
187
187
|
# ClassSingleton (value constant like EXPANDING) is compatible with its base type.
|
|
188
188
|
# e.g., singleton(EXPANDING) ↔ Integer when EXPANDING = 2
|
|
189
|
+
elsif literal_compatible?(t1, t2)
|
|
190
|
+
# Literals are subtypes of their base type (e.g., Literal(1) <: Integer, Literal("x") <: String)
|
|
191
|
+
# Unions of same-type literals are also compatible (e.g., -1 | 0 | 1 <: Integer)
|
|
189
192
|
else
|
|
190
193
|
raise UnificationError.new(t1, t2)
|
|
191
194
|
end
|
|
@@ -207,6 +210,38 @@ module Konpeito
|
|
|
207
210
|
(t2.is_a?(Types::ClassSingleton) && t1.is_a?(Types::ClassInstance))
|
|
208
211
|
end
|
|
209
212
|
|
|
213
|
+
# Check if a literal type (or union of literals) is compatible with its base class type.
|
|
214
|
+
# e.g., Literal(1) <: Integer, Literal("hello") <: String, Literal(1.0) <: Float
|
|
215
|
+
# Union(Literal(-1), Literal(0), Literal(1)) <: Integer
|
|
216
|
+
def literal_compatible?(t1, t2)
|
|
217
|
+
(literal_subtype_of_class?(t1, t2)) || (literal_subtype_of_class?(t2, t1))
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def literal_subtype_of_class?(literal_side, class_side)
|
|
221
|
+
return false unless class_side.is_a?(Types::ClassInstance)
|
|
222
|
+
|
|
223
|
+
if literal_side.is_a?(Types::Literal)
|
|
224
|
+
literal_base_type_name(literal_side.value) == class_side.name
|
|
225
|
+
elsif literal_side.is_a?(Types::Union)
|
|
226
|
+
literal_side.types.all? do |sub|
|
|
227
|
+
sub.is_a?(Types::Literal) && literal_base_type_name(sub.value) == class_side.name
|
|
228
|
+
end
|
|
229
|
+
else
|
|
230
|
+
false
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Map a Ruby literal value to its base type name
|
|
235
|
+
def literal_base_type_name(value)
|
|
236
|
+
case value
|
|
237
|
+
when Integer then :Integer
|
|
238
|
+
when Float then :Float
|
|
239
|
+
when String then :String
|
|
240
|
+
when Symbol then :Symbol
|
|
241
|
+
when true, false then :Bool
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
210
245
|
# Check if `from` can be widened to `to` (Java/Kotlin-style widening primitive conversion).
|
|
211
246
|
# Integer → Float is safe (no precision loss for typical values).
|
|
212
247
|
# Float → Integer is NOT supported (requires explicit conversion).
|
data/lib/konpeito/ui/column.rb
CHANGED
|
@@ -25,6 +25,14 @@ class Column < Layout
|
|
|
25
25
|
#: () -> Column
|
|
26
26
|
def scrollable
|
|
27
27
|
@is_scrollable = true
|
|
28
|
+
# Retroactively downgrade existing EXPANDING children to CONTENT
|
|
29
|
+
i = 0
|
|
30
|
+
while i < @children.length
|
|
31
|
+
if @children[i].get_height_policy == EXPANDING
|
|
32
|
+
@children[i].set_height_policy(CONTENT)
|
|
33
|
+
end
|
|
34
|
+
i = i + 1
|
|
35
|
+
end
|
|
28
36
|
self
|
|
29
37
|
end
|
|
30
38
|
|
|
@@ -60,21 +68,43 @@ class Column < Layout
|
|
|
60
68
|
update
|
|
61
69
|
end
|
|
62
70
|
|
|
71
|
+
# Override add: auto-downgrade EXPANDING height to CONTENT in scrollable Column
|
|
72
|
+
#: (untyped w) -> Column
|
|
73
|
+
def add(w)
|
|
74
|
+
if w == nil
|
|
75
|
+
return self
|
|
76
|
+
end
|
|
77
|
+
if @is_scrollable && w.get_height_policy == EXPANDING
|
|
78
|
+
w.set_height_policy(CONTENT)
|
|
79
|
+
end
|
|
80
|
+
super(w)
|
|
81
|
+
self
|
|
82
|
+
end
|
|
83
|
+
|
|
63
84
|
#: (untyped painter) -> Size
|
|
64
85
|
def measure(painter)
|
|
65
86
|
total_h = 0.0
|
|
66
87
|
max_w = 0.0
|
|
67
88
|
i = 0
|
|
68
89
|
while i < @children.length
|
|
69
|
-
|
|
70
|
-
|
|
90
|
+
c = @children[i]
|
|
91
|
+
cs = c.measure(painter)
|
|
92
|
+
if c.get_height_policy == FIXED
|
|
93
|
+
child_h = c.get_height
|
|
94
|
+
else
|
|
95
|
+
child_h = cs.height
|
|
96
|
+
end
|
|
97
|
+
total_h = total_h + child_h
|
|
71
98
|
total_h = total_h + @spacing if i > 0
|
|
72
99
|
max_w = cs.width if cs.width > max_w
|
|
73
100
|
i = i + 1
|
|
74
101
|
end
|
|
75
|
-
Size.new(max_w, total_h)
|
|
102
|
+
Size.new(max_w + @pad_left + @pad_right, total_h + @pad_top + @pad_bottom)
|
|
76
103
|
end
|
|
77
104
|
|
|
105
|
+
# Unified layout: two-pass flex distribution + scroll offset.
|
|
106
|
+
# With approach C (auto-downgrade), scrollable containers have no EXPANDING
|
|
107
|
+
# children, so flex distribution is a no-op and content stacks sequentially.
|
|
78
108
|
#: (untyped painter) -> void
|
|
79
109
|
def relocate_children(painter)
|
|
80
110
|
# Account for padding
|
|
@@ -90,23 +120,28 @@ class Column < Layout
|
|
|
90
120
|
remaining = inner_h
|
|
91
121
|
expanding_total_flex = 0
|
|
92
122
|
|
|
93
|
-
# First pass: measure FIXED
|
|
94
|
-
# In a Column, non-FIXED children fill the column width (for centering, word-wrap, etc.)
|
|
123
|
+
# First pass: measure CONTENT/FIXED children, collect EXPANDING flex totals
|
|
95
124
|
i = 0
|
|
96
125
|
while i < @children.length
|
|
97
126
|
c = @children[i]
|
|
98
127
|
if c.get_height_policy != EXPANDING
|
|
99
|
-
# Set width before measure
|
|
128
|
+
# Set width before measure (for word-wrap, centering, etc.)
|
|
100
129
|
if c.get_width_policy != FIXED
|
|
101
130
|
c.resize_wh(inner_w, c.get_height)
|
|
102
131
|
end
|
|
103
132
|
cs = c.measure(painter)
|
|
133
|
+
# Use explicit height for FIXED, measured height for CONTENT
|
|
134
|
+
if c.get_height_policy == FIXED
|
|
135
|
+
child_h = c.get_height
|
|
136
|
+
else
|
|
137
|
+
child_h = cs.height
|
|
138
|
+
end
|
|
104
139
|
if c.get_width_policy == FIXED
|
|
105
|
-
c.resize_wh(cs.width,
|
|
140
|
+
c.resize_wh(cs.width, child_h)
|
|
106
141
|
else
|
|
107
|
-
c.resize_wh(inner_w,
|
|
142
|
+
c.resize_wh(inner_w, child_h)
|
|
108
143
|
end
|
|
109
|
-
remaining = remaining -
|
|
144
|
+
remaining = remaining - child_h
|
|
110
145
|
else
|
|
111
146
|
expanding_total_flex = expanding_total_flex + c.get_flex
|
|
112
147
|
end
|
|
@@ -118,7 +153,7 @@ class Column < Layout
|
|
|
118
153
|
remaining = 0.0
|
|
119
154
|
end
|
|
120
155
|
|
|
121
|
-
# Second pass: distribute remaining space to EXPANDING
|
|
156
|
+
# Second pass: distribute remaining space to EXPANDING, position all
|
|
122
157
|
cy = @y + @pad_top
|
|
123
158
|
if @is_scrollable
|
|
124
159
|
cy = cy - @scroll_offset
|
data/lib/konpeito/ui/core.rb
CHANGED
|
@@ -1658,6 +1658,16 @@ class Widget
|
|
|
1658
1658
|
@enable_to_detach = false
|
|
1659
1659
|
end
|
|
1660
1660
|
|
|
1661
|
+
#: (bool v) -> void
|
|
1662
|
+
def set_cached(v)
|
|
1663
|
+
@cached = v
|
|
1664
|
+
end
|
|
1665
|
+
|
|
1666
|
+
#: () -> bool
|
|
1667
|
+
def is_cached
|
|
1668
|
+
@cached
|
|
1669
|
+
end
|
|
1670
|
+
|
|
1661
1671
|
# --- Observer Protocol ---
|
|
1662
1672
|
|
|
1663
1673
|
#: (untyped o) -> void
|
|
@@ -1923,6 +1933,9 @@ class Layout < Widget
|
|
|
1923
1933
|
|
|
1924
1934
|
#: (untyped w) -> Layout
|
|
1925
1935
|
def add(w)
|
|
1936
|
+
if w == nil
|
|
1937
|
+
return self
|
|
1938
|
+
end
|
|
1926
1939
|
# Remove from old parent if needed
|
|
1927
1940
|
old_parent = w.get_parent
|
|
1928
1941
|
if old_parent != nil && old_parent != self
|
|
@@ -2103,9 +2116,144 @@ class Layout < Widget
|
|
|
2103
2116
|
end
|
|
2104
2117
|
end
|
|
2105
2118
|
|
|
2119
|
+
# ===== BuildOwner =====
|
|
2120
|
+
# Port of ~/castella/castella/build_owner.py BuildOwner
|
|
2121
|
+
# Batches multiple state changes into a single rebuild pass.
|
|
2122
|
+
#
|
|
2123
|
+
# Usage:
|
|
2124
|
+
# owner = BuildOwner.get
|
|
2125
|
+
# owner.build_scope {
|
|
2126
|
+
# state1.set(value1)
|
|
2127
|
+
# state2.set(value2)
|
|
2128
|
+
# }
|
|
2129
|
+
# # → Only ONE rebuild for all affected components
|
|
2130
|
+
|
|
2131
|
+
class BuildOwner
|
|
2132
|
+
@@instance = nil
|
|
2133
|
+
|
|
2134
|
+
#: () -> BuildOwner
|
|
2135
|
+
def self.get
|
|
2136
|
+
if @@instance == nil
|
|
2137
|
+
@@instance = BuildOwner.new
|
|
2138
|
+
end
|
|
2139
|
+
@@instance
|
|
2140
|
+
end
|
|
2141
|
+
|
|
2142
|
+
#: () -> void
|
|
2143
|
+
def self.reset
|
|
2144
|
+
@@instance = nil
|
|
2145
|
+
end
|
|
2146
|
+
|
|
2147
|
+
def initialize
|
|
2148
|
+
@dirty_components = []
|
|
2149
|
+
@in_build_scope = false
|
|
2150
|
+
@scope_depth = 0
|
|
2151
|
+
end
|
|
2152
|
+
|
|
2153
|
+
#: () -> bool
|
|
2154
|
+
def is_in_build_scope
|
|
2155
|
+
@in_build_scope
|
|
2156
|
+
end
|
|
2157
|
+
|
|
2158
|
+
# Schedule a component for rebuild. Deduplicates.
|
|
2159
|
+
# Inside build_scope: just adds to dirty list.
|
|
2160
|
+
# Outside build_scope: immediate mode (backward compatibility).
|
|
2161
|
+
#: (untyped component) -> void
|
|
2162
|
+
def schedule_build_for(component)
|
|
2163
|
+
# Dedup: skip if already in dirty list
|
|
2164
|
+
i = 0
|
|
2165
|
+
while i < @dirty_components.length
|
|
2166
|
+
if @dirty_components[i] == component
|
|
2167
|
+
return
|
|
2168
|
+
end
|
|
2169
|
+
i = i + 1
|
|
2170
|
+
end
|
|
2171
|
+
|
|
2172
|
+
if !@in_build_scope
|
|
2173
|
+
# Immediate mode: mark and trigger redraw now (don't accumulate)
|
|
2174
|
+
component.mark_pending_rebuild
|
|
2175
|
+
app = App.current
|
|
2176
|
+
if app != nil
|
|
2177
|
+
app.post_update(component)
|
|
2178
|
+
end
|
|
2179
|
+
else
|
|
2180
|
+
# Batched mode: just add to dirty list for flush_builds
|
|
2181
|
+
@dirty_components << component
|
|
2182
|
+
end
|
|
2183
|
+
end
|
|
2184
|
+
|
|
2185
|
+
# Execute a block with batched rebuilds.
|
|
2186
|
+
# Supports nesting: only the outermost scope triggers flush.
|
|
2187
|
+
#: () -> void
|
|
2188
|
+
def build_scope
|
|
2189
|
+
@scope_depth = @scope_depth + 1
|
|
2190
|
+
@in_build_scope = true
|
|
2191
|
+
yield
|
|
2192
|
+
@scope_depth = @scope_depth - 1
|
|
2193
|
+
if @scope_depth == 0
|
|
2194
|
+
@in_build_scope = false
|
|
2195
|
+
flush_builds
|
|
2196
|
+
end
|
|
2197
|
+
end
|
|
2198
|
+
|
|
2199
|
+
# Process all pending rebuilds: mark as pending, trigger one redraw.
|
|
2200
|
+
# Components are sorted by depth (parents before children) so parent
|
|
2201
|
+
# rebuilds don't cause redundant child rebuilds.
|
|
2202
|
+
#: () -> void
|
|
2203
|
+
def flush_builds
|
|
2204
|
+
while @dirty_components.length > 0
|
|
2205
|
+
# Sort by depth (parents first)
|
|
2206
|
+
sorted = sort_by_depth(@dirty_components)
|
|
2207
|
+
@dirty_components = []
|
|
2208
|
+
|
|
2209
|
+
# Mark all as pending rebuild
|
|
2210
|
+
i = 0
|
|
2211
|
+
while i < sorted.length
|
|
2212
|
+
sorted[i].mark_pending_rebuild
|
|
2213
|
+
i = i + 1
|
|
2214
|
+
end
|
|
2215
|
+
|
|
2216
|
+
# Trigger single redraw
|
|
2217
|
+
if sorted.length > 0
|
|
2218
|
+
app = App.current
|
|
2219
|
+
if app != nil
|
|
2220
|
+
app.post_update(sorted[0])
|
|
2221
|
+
end
|
|
2222
|
+
end
|
|
2223
|
+
end
|
|
2224
|
+
end
|
|
2225
|
+
|
|
2226
|
+
private
|
|
2227
|
+
|
|
2228
|
+
# Insertion sort by widget depth (ascending)
|
|
2229
|
+
#: (Array components) -> Array
|
|
2230
|
+
def sort_by_depth(components)
|
|
2231
|
+
result = []
|
|
2232
|
+
i = 0
|
|
2233
|
+
while i < components.length
|
|
2234
|
+
result << components[i]
|
|
2235
|
+
i = i + 1
|
|
2236
|
+
end
|
|
2237
|
+
i = 1
|
|
2238
|
+
while i < result.length
|
|
2239
|
+
j = i
|
|
2240
|
+
while j > 0
|
|
2241
|
+
if result[j].get_depth < result[j - 1].get_depth
|
|
2242
|
+
tmp = result[j]
|
|
2243
|
+
result[j] = result[j - 1]
|
|
2244
|
+
result[j - 1] = tmp
|
|
2245
|
+
end
|
|
2246
|
+
j = j - 1
|
|
2247
|
+
end
|
|
2248
|
+
i = i + 1
|
|
2249
|
+
end
|
|
2250
|
+
result
|
|
2251
|
+
end
|
|
2252
|
+
end
|
|
2253
|
+
|
|
2106
2254
|
# ===== Component =====
|
|
2107
2255
|
# Port of ~/castella/castella/core.py Component
|
|
2108
|
-
# Now with
|
|
2256
|
+
# Now with cache() for widget reuse and BuildOwner integration
|
|
2109
2257
|
|
|
2110
2258
|
class Component < Layout
|
|
2111
2259
|
def initialize
|
|
@@ -2114,6 +2262,8 @@ class Component < Layout
|
|
|
2114
2262
|
@height_policy = EXPANDING
|
|
2115
2263
|
@child = nil
|
|
2116
2264
|
@pending_rebuild = false
|
|
2265
|
+
@cache_data = [] # Array of [keys_array, widgets_array] pairs per cache() call
|
|
2266
|
+
@cache_counter = 0 # Reset each view() call
|
|
2117
2267
|
end
|
|
2118
2268
|
|
|
2119
2269
|
# Helper: create State + auto-attach
|
|
@@ -2130,22 +2280,116 @@ class Component < Layout
|
|
|
2130
2280
|
nil
|
|
2131
2281
|
end
|
|
2132
2282
|
|
|
2133
|
-
#
|
|
2283
|
+
# Cache widget instances across view() rebuilds.
|
|
2284
|
+
# Returns an array of widgets, reusing existing ones for matching items.
|
|
2285
|
+
# Items are matched by == comparison.
|
|
2286
|
+
#
|
|
2287
|
+
# Usage in view():
|
|
2288
|
+
# widgets = cache(items) { |item| Text.new(item.label) }
|
|
2289
|
+
# i = 0
|
|
2290
|
+
# while i < widgets.length
|
|
2291
|
+
# embed(widgets[i])
|
|
2292
|
+
# i = i + 1
|
|
2293
|
+
# end
|
|
2294
|
+
#
|
|
2295
|
+
#: (Array items) -> Array
|
|
2296
|
+
def cache(items)
|
|
2297
|
+
slot = @cache_counter
|
|
2298
|
+
@cache_counter = @cache_counter + 1
|
|
2299
|
+
|
|
2300
|
+
# Get old cache for this slot
|
|
2301
|
+
old_keys = nil
|
|
2302
|
+
old_widgets = nil
|
|
2303
|
+
if slot < @cache_data.length
|
|
2304
|
+
entry = @cache_data[slot]
|
|
2305
|
+
if entry != nil
|
|
2306
|
+
old_keys = entry[0]
|
|
2307
|
+
old_widgets = entry[1]
|
|
2308
|
+
end
|
|
2309
|
+
end
|
|
2310
|
+
|
|
2311
|
+
new_keys = []
|
|
2312
|
+
new_widgets = []
|
|
2313
|
+
|
|
2314
|
+
i = 0
|
|
2315
|
+
while i < items.length
|
|
2316
|
+
item = items[i]
|
|
2317
|
+
# Look up in old cache by == comparison
|
|
2318
|
+
found = nil
|
|
2319
|
+
if old_keys != nil
|
|
2320
|
+
j = 0
|
|
2321
|
+
while j < old_keys.length
|
|
2322
|
+
if old_keys[j] != nil && old_keys[j] == item
|
|
2323
|
+
found = old_widgets[j]
|
|
2324
|
+
old_keys[j] = nil # Mark as used
|
|
2325
|
+
break
|
|
2326
|
+
end
|
|
2327
|
+
j = j + 1
|
|
2328
|
+
end
|
|
2329
|
+
end
|
|
2330
|
+
|
|
2331
|
+
if found != nil
|
|
2332
|
+
found.set_cached(true) # Safety: prevent do_unmount if somehow reached
|
|
2333
|
+
new_keys << item
|
|
2334
|
+
new_widgets << found
|
|
2335
|
+
else
|
|
2336
|
+
widget = yield(item)
|
|
2337
|
+
new_keys << item
|
|
2338
|
+
new_widgets << widget
|
|
2339
|
+
end
|
|
2340
|
+
i = i + 1
|
|
2341
|
+
end
|
|
2342
|
+
|
|
2343
|
+
# Old widgets not reused will be detached when old tree is destroyed.
|
|
2344
|
+
# Clear cached flag so they can be properly cleaned up.
|
|
2345
|
+
if old_keys != nil
|
|
2346
|
+
j = 0
|
|
2347
|
+
while j < old_keys.length
|
|
2348
|
+
if old_keys[j] != nil
|
|
2349
|
+
old_widgets[j].set_cached(false)
|
|
2350
|
+
end
|
|
2351
|
+
j = j + 1
|
|
2352
|
+
end
|
|
2353
|
+
end
|
|
2354
|
+
|
|
2355
|
+
# Store updated cache
|
|
2356
|
+
while @cache_data.length <= slot
|
|
2357
|
+
@cache_data << nil
|
|
2358
|
+
end
|
|
2359
|
+
@cache_data[slot] = [new_keys, new_widgets]
|
|
2360
|
+
|
|
2361
|
+
new_widgets
|
|
2362
|
+
end
|
|
2363
|
+
|
|
2364
|
+
# Mark this component as needing rebuild (called by BuildOwner)
|
|
2134
2365
|
#: () -> void
|
|
2135
|
-
def
|
|
2366
|
+
def mark_pending_rebuild
|
|
2136
2367
|
@pending_rebuild = true
|
|
2137
2368
|
mark_paint_dirty
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2369
|
+
end
|
|
2370
|
+
|
|
2371
|
+
# State change notification -> route to BuildOwner for batched rebuild
|
|
2372
|
+
#: () -> void
|
|
2373
|
+
def on_notify
|
|
2374
|
+
owner = BuildOwner.get
|
|
2375
|
+
owner.schedule_build_for(self)
|
|
2142
2376
|
end
|
|
2143
2377
|
|
|
2144
2378
|
#: (untyped painter, bool completely) -> void
|
|
2145
2379
|
def redraw(painter, completely)
|
|
2146
|
-
|
|
2380
|
+
needs_build = false
|
|
2147
2381
|
if @pending_rebuild
|
|
2148
2382
|
@pending_rebuild = false
|
|
2383
|
+
needs_build = true
|
|
2384
|
+
end
|
|
2385
|
+
if @child == nil
|
|
2386
|
+
needs_build = true
|
|
2387
|
+
end
|
|
2388
|
+
|
|
2389
|
+
if needs_build
|
|
2390
|
+
# Reset cache counter for view()
|
|
2391
|
+
@cache_counter = 0
|
|
2392
|
+
|
|
2149
2393
|
# Save focused widget's tab_index
|
|
2150
2394
|
saved_focus_tab = -1
|
|
2151
2395
|
app = App.current
|
|
@@ -2156,37 +2400,39 @@ class Component < Layout
|
|
|
2156
2400
|
end
|
|
2157
2401
|
end
|
|
2158
2402
|
|
|
2159
|
-
#
|
|
2403
|
+
# Build new tree FIRST (cache() may reuse widgets from old tree).
|
|
2404
|
+
# Reused widgets are removed from old tree by Layout#add() when
|
|
2405
|
+
# they are added to the new tree, so they won't be affected by
|
|
2406
|
+
# the subsequent old tree destruction.
|
|
2407
|
+
new_child = view
|
|
2408
|
+
|
|
2409
|
+
# Destroy old tree (cached widgets already removed from it)
|
|
2160
2410
|
if @child != nil
|
|
2161
2411
|
remove(@child)
|
|
2162
2412
|
@child.detach
|
|
2163
2413
|
@child = nil
|
|
2164
2414
|
end
|
|
2165
|
-
|
|
2415
|
+
|
|
2416
|
+
# Install new tree
|
|
2417
|
+
@child = new_child
|
|
2166
2418
|
if @child != nil
|
|
2167
2419
|
add(@child)
|
|
2168
2420
|
completely = true
|
|
2169
2421
|
end
|
|
2170
2422
|
|
|
2171
2423
|
# Restore focus (text restoration not needed — InputState persists)
|
|
2172
|
-
if saved_focus_tab > 0
|
|
2173
|
-
|
|
2174
|
-
if
|
|
2175
|
-
|
|
2176
|
-
focus_target
|
|
2424
|
+
if saved_focus_tab > 0
|
|
2425
|
+
app = App.current
|
|
2426
|
+
if app != nil
|
|
2427
|
+
focus_target = find_focusable_by_tab_index(@child, saved_focus_tab)
|
|
2428
|
+
if focus_target != nil
|
|
2429
|
+
app.set_focused(focus_target)
|
|
2430
|
+
focus_target.restore_focus
|
|
2431
|
+
end
|
|
2177
2432
|
end
|
|
2178
2433
|
end
|
|
2179
2434
|
end
|
|
2180
2435
|
|
|
2181
|
-
# Build view if needed
|
|
2182
|
-
if @child == nil
|
|
2183
|
-
@child = view
|
|
2184
|
-
if @child != nil
|
|
2185
|
-
add(@child)
|
|
2186
|
-
completely = true
|
|
2187
|
-
end
|
|
2188
|
-
end
|
|
2189
|
-
|
|
2190
2436
|
# Relocate + redraw
|
|
2191
2437
|
if @children.length > 0
|
|
2192
2438
|
relocate_children(painter)
|
data/lib/konpeito/ui/row.rb
CHANGED
|
@@ -22,6 +22,14 @@ class Row < Layout
|
|
|
22
22
|
#: () -> Row
|
|
23
23
|
def scrollable
|
|
24
24
|
@is_scrollable = true
|
|
25
|
+
# Retroactively downgrade existing EXPANDING children to CONTENT
|
|
26
|
+
i = 0
|
|
27
|
+
while i < @children.length
|
|
28
|
+
if @children[i].get_width_policy == EXPANDING
|
|
29
|
+
@children[i].set_width_policy(CONTENT)
|
|
30
|
+
end
|
|
31
|
+
i = i + 1
|
|
32
|
+
end
|
|
25
33
|
self
|
|
26
34
|
end
|
|
27
35
|
|
|
@@ -57,21 +65,48 @@ class Row < Layout
|
|
|
57
65
|
update
|
|
58
66
|
end
|
|
59
67
|
|
|
68
|
+
# Override add: auto-downgrade EXPANDING width to CONTENT in scrollable Row
|
|
69
|
+
#: (untyped w) -> Row
|
|
70
|
+
def add(w)
|
|
71
|
+
if w == nil
|
|
72
|
+
return self
|
|
73
|
+
end
|
|
74
|
+
if @is_scrollable && w.get_width_policy == EXPANDING
|
|
75
|
+
w.set_width_policy(CONTENT)
|
|
76
|
+
end
|
|
77
|
+
super(w)
|
|
78
|
+
self
|
|
79
|
+
end
|
|
80
|
+
|
|
60
81
|
#: (untyped painter) -> Size
|
|
61
82
|
def measure(painter)
|
|
62
83
|
total_w = 0.0
|
|
63
84
|
max_h = 0.0
|
|
64
85
|
i = 0
|
|
65
86
|
while i < @children.length
|
|
66
|
-
|
|
67
|
-
|
|
87
|
+
c = @children[i]
|
|
88
|
+
cs = c.measure(painter)
|
|
89
|
+
if c.get_width_policy == FIXED
|
|
90
|
+
child_w = c.get_width
|
|
91
|
+
else
|
|
92
|
+
child_w = cs.width
|
|
93
|
+
end
|
|
94
|
+
total_w = total_w + child_w
|
|
68
95
|
total_w = total_w + @spacing if i > 0
|
|
69
|
-
|
|
96
|
+
if c.get_height_policy == FIXED
|
|
97
|
+
child_h = c.get_height
|
|
98
|
+
else
|
|
99
|
+
child_h = cs.height
|
|
100
|
+
end
|
|
101
|
+
max_h = child_h if child_h > max_h
|
|
70
102
|
i = i + 1
|
|
71
103
|
end
|
|
72
|
-
Size.new(total_w, max_h)
|
|
104
|
+
Size.new(total_w + @pad_left + @pad_right, max_h + @pad_top + @pad_bottom)
|
|
73
105
|
end
|
|
74
106
|
|
|
107
|
+
# Unified layout: two-pass flex distribution + scroll offset.
|
|
108
|
+
# With approach C (auto-downgrade), scrollable containers have no EXPANDING
|
|
109
|
+
# width children, so flex distribution is a no-op and content stacks sequentially.
|
|
75
110
|
#: (untyped painter) -> void
|
|
76
111
|
def relocate_children(painter)
|
|
77
112
|
# Account for padding
|
|
@@ -87,22 +122,28 @@ class Row < Layout
|
|
|
87
122
|
remaining = inner_w
|
|
88
123
|
expanding_total_flex = 0
|
|
89
124
|
|
|
90
|
-
# First pass: measure FIXED
|
|
125
|
+
# First pass: measure CONTENT/FIXED children, collect EXPANDING flex totals
|
|
91
126
|
i = 0
|
|
92
127
|
while i < @children.length
|
|
93
128
|
c = @children[i]
|
|
94
129
|
if c.get_width_policy != EXPANDING
|
|
95
130
|
# Set height before measure so height-dependent layouts work
|
|
96
|
-
if c.get_height_policy
|
|
131
|
+
if c.get_height_policy != FIXED
|
|
97
132
|
c.resize_wh(c.get_width, inner_h)
|
|
98
133
|
end
|
|
99
134
|
cs = c.measure(painter)
|
|
100
|
-
|
|
101
|
-
|
|
135
|
+
# Use explicit width for FIXED, measured width for CONTENT
|
|
136
|
+
if c.get_width_policy == FIXED
|
|
137
|
+
child_w = c.get_width
|
|
138
|
+
else
|
|
139
|
+
child_w = cs.width
|
|
140
|
+
end
|
|
141
|
+
if c.get_height_policy == FIXED
|
|
142
|
+
c.resize_wh(child_w, c.get_height)
|
|
102
143
|
else
|
|
103
|
-
c.resize_wh(
|
|
144
|
+
c.resize_wh(child_w, inner_h)
|
|
104
145
|
end
|
|
105
|
-
remaining = remaining -
|
|
146
|
+
remaining = remaining - child_w
|
|
106
147
|
else
|
|
107
148
|
expanding_total_flex = expanding_total_flex + c.get_flex
|
|
108
149
|
end
|
|
@@ -114,7 +155,7 @@ class Row < Layout
|
|
|
114
155
|
remaining = 0.0
|
|
115
156
|
end
|
|
116
157
|
|
|
117
|
-
# Second pass: distribute remaining space, position all
|
|
158
|
+
# Second pass: distribute remaining space to EXPANDING, position all
|
|
118
159
|
cx = @x + @pad_left
|
|
119
160
|
if @is_scrollable
|
|
120
161
|
cx = cx - @scroll_offset
|
|
@@ -130,7 +171,8 @@ class Row < Layout
|
|
|
130
171
|
end
|
|
131
172
|
c.resize_wh(w, inner_h)
|
|
132
173
|
else
|
|
133
|
-
|
|
174
|
+
# In a Row, non-FIXED height children fill the row height
|
|
175
|
+
if c.get_height_policy != FIXED
|
|
134
176
|
c.resize_wh(c.get_width, inner_h)
|
|
135
177
|
end
|
|
136
178
|
end
|
data/lib/konpeito/ui/theme.rb
CHANGED
|
@@ -50,7 +50,7 @@ class Theme
|
|
|
50
50
|
@text_on_danger = 0xFF1A1B26
|
|
51
51
|
|
|
52
52
|
# Kind-specific hover colors
|
|
53
|
-
@hover_normal =
|
|
53
|
+
@hover_normal = 0xFF89B4FA
|
|
54
54
|
@hover_info = 0xFF89B4FA
|
|
55
55
|
@hover_success = 0xFFB9F27C
|
|
56
56
|
@hover_warning = 0xFFFFD280
|
|
@@ -462,7 +462,7 @@ def theme_light
|
|
|
462
462
|
t.hover_success = 0xFF567236
|
|
463
463
|
t.hover_warning = 0xFFA87020
|
|
464
464
|
t.hover_danger = 0xFFA35060
|
|
465
|
-
t.hover_normal =
|
|
465
|
+
t.hover_normal = 0xFF4A6EA0
|
|
466
466
|
t.scrollbar_bg = 0xFFC4C5CB
|
|
467
467
|
t.scrollbar_fg = 0xFF9699A3
|
|
468
468
|
t.bg_selected = 0x8034548A
|
|
@@ -496,7 +496,7 @@ def theme_nord
|
|
|
496
496
|
t.hover_success = 0xFFB4D89C
|
|
497
497
|
t.hover_warning = 0xFFF5D9A0
|
|
498
498
|
t.hover_danger = 0xFFD08770
|
|
499
|
-
t.hover_normal =
|
|
499
|
+
t.hover_normal = 0xFF9DD0DE
|
|
500
500
|
t
|
|
501
501
|
end
|
|
502
502
|
|
|
@@ -527,7 +527,7 @@ def theme_dracula
|
|
|
527
527
|
t.hover_success = 0xFF69FF94
|
|
528
528
|
t.hover_warning = 0xFFFFFFA5
|
|
529
529
|
t.hover_danger = 0xFFFF6E6E
|
|
530
|
-
t.hover_normal =
|
|
530
|
+
t.hover_normal = 0xFFD0ABFF
|
|
531
531
|
t
|
|
532
532
|
end
|
|
533
533
|
|
|
@@ -558,6 +558,6 @@ def theme_catppuccin
|
|
|
558
558
|
t.hover_success = 0xFFB9F0B4
|
|
559
559
|
t.hover_warning = 0xFFFFF0CC
|
|
560
560
|
t.hover_danger = 0xFFFFA0B8
|
|
561
|
-
t.hover_normal =
|
|
561
|
+
t.hover_normal = 0xFFDEC0FF
|
|
562
562
|
t
|
|
563
563
|
end
|
|
@@ -15,8 +15,6 @@ class Button < Widget
|
|
|
15
15
|
@radius = 4.0
|
|
16
16
|
@hovered = false
|
|
17
17
|
@click_handler = nil
|
|
18
|
-
@width_policy = CONTENT
|
|
19
|
-
@height_policy = CONTENT
|
|
20
18
|
@pad_top = 8.0
|
|
21
19
|
@pad_right = 16.0
|
|
22
20
|
@pad_bottom = 8.0
|
|
@@ -76,8 +74,10 @@ class Button < Widget
|
|
|
76
74
|
painter.fill_round_rect(0.0, 0.0, @width, @height, @radius, bg_c)
|
|
77
75
|
ascent = painter.get_text_ascent($theme.font_family, @font_size_val)
|
|
78
76
|
th = painter.measure_text_height($theme.font_family, @font_size_val)
|
|
77
|
+
tw = painter.measure_text_width(@label, $theme.font_family, @font_size_val)
|
|
78
|
+
text_x = (@width - tw) / 2.0
|
|
79
79
|
text_y = (@height - th) / 2.0 + ascent
|
|
80
|
-
painter.draw_text(@label,
|
|
80
|
+
painter.draw_text(@label, text_x, text_y, $theme.font_family, @font_size_val, tc)
|
|
81
81
|
end
|
|
82
82
|
|
|
83
83
|
def mouse_up(ev)
|
|
@@ -42,6 +42,24 @@ class Container < Layout
|
|
|
42
42
|
self
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
+
def measure(painter)
|
|
46
|
+
if @children.length > 0
|
|
47
|
+
c = @children[0]
|
|
48
|
+
inner_w = @width - @pad_left - @pad_right
|
|
49
|
+
if inner_w < 0.0
|
|
50
|
+
inner_w = 0.0
|
|
51
|
+
end
|
|
52
|
+
# Set child width before measuring for word-wrap support
|
|
53
|
+
if c.get_width_policy != FIXED
|
|
54
|
+
c.resize_wh(inner_w, c.get_height)
|
|
55
|
+
end
|
|
56
|
+
cs = c.measure(painter)
|
|
57
|
+
Size.new(cs.width + @pad_left + @pad_right, cs.height + @pad_top + @pad_bottom)
|
|
58
|
+
else
|
|
59
|
+
Size.new(@pad_left + @pad_right, @pad_top + @pad_bottom)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
45
63
|
def relocate_children(painter)
|
|
46
64
|
if @children.length > 0
|
|
47
65
|
c = @children[0]
|
|
@@ -18,8 +18,6 @@ class Text < Widget
|
|
|
18
18
|
@text_align = TEXT_ALIGN_LEFT
|
|
19
19
|
@font_weight = 0
|
|
20
20
|
@font_slant = 0
|
|
21
|
-
@width_policy = CONTENT
|
|
22
|
-
@height_policy = CONTENT
|
|
23
21
|
end
|
|
24
22
|
|
|
25
23
|
def font_size(s)
|
|
@@ -85,6 +83,16 @@ class Text < Widget
|
|
|
85
83
|
def redraw(painter, completely)
|
|
86
84
|
ff = resolved_font_family
|
|
87
85
|
ascent = painter.get_text_ascent(ff, @font_size_val)
|
|
86
|
+
th = painter.measure_text_height(ff, @font_size_val)
|
|
87
|
+
|
|
88
|
+
# Vertical centering when widget is taller than text
|
|
89
|
+
if @height > th
|
|
90
|
+
y_offset = (@height - th) / 2.0 + ascent
|
|
91
|
+
else
|
|
92
|
+
y_offset = ascent
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Horizontal alignment
|
|
88
96
|
x_offset = 0.0
|
|
89
97
|
if @text_align == TEXT_ALIGN_CENTER
|
|
90
98
|
text_w = painter.measure_text_width(@text, ff, @font_size_val)
|
|
@@ -100,7 +108,7 @@ class Text < Widget
|
|
|
100
108
|
end
|
|
101
109
|
end
|
|
102
110
|
c = @custom_color ? @color_val : $theme.text_color_for_kind(@kind_val)
|
|
103
|
-
painter.draw_text(@text, x_offset,
|
|
111
|
+
painter.draw_text(@text, x_offset, y_offset, ff, @font_size_val, c, @font_weight, @font_slant)
|
|
104
112
|
end
|
|
105
113
|
end
|
|
106
114
|
|
data/lib/konpeito/version.rb
CHANGED