Skip to content

Commit bf6aa73

Browse files
committed
add numeric input view; wire step/locale; update demo/docs; remove DATA_GRID_PLAN
1 parent f4f0e6d commit bf6aa73

10 files changed

Lines changed: 865 additions & 128 deletions

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ v run examples/dialogs.v # Custom + native dialogs
167167
v run examples/split_panel.v # Splitter + nested splitter
168168
v run examples/printing.v # PDF export + native print
169169
v run examples/input_masks.v # Input mask presets and custom tokens
170+
v run examples/numeric_input.v # Locale-aware numeric input + step controls
170171
v run examples/text_transform.v # Rotated and affine text
171172
v run examples/table_demo.v # Table widget demo
172173
v run examples/data_grid_demo.v # Data grid widget demo

_view_numeric_input_test.v

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
module gui
2+
3+
import math
4+
5+
fn test_numeric_parse_en_locale() {
6+
value := numeric_parse('1,234.50', NumericLocaleCfg{}) or {
7+
assert false
8+
return
9+
}
10+
assert math.abs(value - 1234.5) < 0.000001
11+
}
12+
13+
fn test_numeric_parse_de_locale() {
14+
locale := NumericLocaleCfg{
15+
decimal_sep: `,`
16+
group_sep: `.`
17+
}
18+
value := numeric_parse('1.234,50', locale) or {
19+
assert false
20+
return
21+
}
22+
assert math.abs(value - 1234.5) < 0.000001
23+
}
24+
25+
fn test_numeric_parse_invalid_string() {
26+
assert numeric_parse('1,,234', NumericLocaleCfg{}) == none
27+
assert numeric_parse('abc', NumericLocaleCfg{}) == none
28+
}
29+
30+
fn test_numeric_format_en_locale() {
31+
assert numeric_format(1234.5, 2, NumericLocaleCfg{}) == '1,234.50'
32+
}
33+
34+
fn test_numeric_format_de_locale() {
35+
locale := NumericLocaleCfg{
36+
decimal_sep: `,`
37+
group_sep: `.`
38+
}
39+
assert numeric_format(1234.5, 2, locale) == '1.234,50'
40+
}
41+
42+
fn test_numeric_format_group_sizes() {
43+
locale := NumericLocaleCfg{
44+
group_sep: `,`
45+
group_sizes: [3, 2]
46+
}
47+
assert numeric_format(1234567, 0, locale) == '12,34,567'
48+
}
49+
50+
fn test_numeric_commit_result_clamps() {
51+
value, text := numeric_input_commit_result('1,250.30', none, 0.0, 1000.0, 2, NumericLocaleCfg{})
52+
assert text == '1,000.00'
53+
if parsed := value {
54+
assert math.abs(parsed - 1000.0) < 0.000001
55+
} else {
56+
assert false
57+
}
58+
}
59+
60+
fn test_numeric_commit_result_invalid_fallback_to_value() {
61+
value, text := numeric_input_commit_result('abc', 12.5, none, none, 1, NumericLocaleCfg{})
62+
assert text == '12.5'
63+
if parsed := value {
64+
assert math.abs(parsed - 12.5) < 0.000001
65+
} else {
66+
assert false
67+
}
68+
}
69+
70+
fn test_numeric_commit_result_invalid_without_value() {
71+
value, text := numeric_input_commit_result('abc', none, none, none, 2, NumericLocaleCfg{})
72+
assert value == none
73+
assert text == ''
74+
}
75+
76+
fn test_numeric_step_result_uses_min_seed() {
77+
value, text := numeric_input_step_result('', none, 10.0, none, 2, NumericStepCfg{},
78+
NumericLocaleCfg{}, 1.0, .none)
79+
assert text == '11.00'
80+
if parsed := value {
81+
assert math.abs(parsed - 11.0) < 0.000001
82+
} else {
83+
assert false
84+
}
85+
}
86+
87+
fn test_numeric_step_result_modifiers() {
88+
cfg := NumericStepCfg{
89+
step: 1.0
90+
shift_multiplier: 10.0
91+
alt_multiplier: 0.1
92+
}
93+
value_shift, text_shift := numeric_input_step_result('5', none, none, none, 2, cfg,
94+
NumericLocaleCfg{}, 1.0, .shift)
95+
assert text_shift == '15.00'
96+
if parsed := value_shift {
97+
assert math.abs(parsed - 15.0) < 0.000001
98+
} else {
99+
assert false
100+
}
101+
102+
value_alt, text_alt := numeric_input_step_result('5', none, none, none, 2, cfg, NumericLocaleCfg{},
103+
1.0, .alt)
104+
assert text_alt == '5.10'
105+
if parsed := value_alt {
106+
assert math.abs(parsed - 5.1) < 0.000001
107+
} else {
108+
assert false
109+
}
110+
}

docs/DATA_GRID_PLAN.md

Lines changed: 0 additions & 83 deletions
This file was deleted.

docs/GET_STARTED.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,43 @@ gui.input(
277277
)
278278
```
279279

280+
## Numeric Input
281+
282+
`gui.numeric_input` provides locale-aware numeric parsing/formatting and step controls.
283+
284+
Fields:
285+
- `value ?f64`
286+
- `locale NumericLocaleCfg`
287+
- `step_cfg NumericStepCfg`
288+
- `decimals int`
289+
- `min ?f64` / `max ?f64`
290+
- `on_value_commit fn (&Layout, ?f64, string, mut Window)`
291+
292+
Rules:
293+
- raw text can be temporarily invalid while editing
294+
- commit (`enter` / blur / step) parses, clamps, formats, emits `on_value_commit`
295+
- step controls support buttons, mouse wheel, and `up/down` keys
296+
- `shift` multiplies step by `10`, `alt` multiplies step by `0.1`
297+
298+
```v ignore
299+
gui.numeric_input(
300+
id_focus: 1
301+
text: app.amount_text
302+
value: app.amount_value
303+
decimals: 2
304+
min: 0.0
305+
max: 10000.0
306+
on_text_changed: fn (_ &gui.Layout, text string, mut w gui.Window) {
307+
w.state[App]().amount_text = text
308+
}
309+
on_value_commit: fn (_ &gui.Layout, value ?f64, text string, mut w gui.Window) {
310+
mut app := w.state[App]()
311+
app.amount_value = value
312+
app.amount_text = text
313+
}
314+
)
315+
```
316+
280317
## Why v-gui Feels Different
281318

282319
**No widget objects to manage.** Traditional frameworks make you create button objects, store
@@ -309,6 +346,7 @@ v run examples/animations.v # Tweens, springs, hero transitions
309346
v run examples/svg_demo.v # Vector icon rendering
310347
v run examples/tiger.v # Complex SVG (Ghostscript Tiger)
311348
v run examples/markdown.v # Markdown rendering
349+
v run examples/numeric_input.v # Locale-aware numeric input + stepping
312350
v run examples/dialogs.v # Custom + native dialogs
313351
v run examples/text_transform.v # Rotated and affine text
314352
v run examples/table_demo.v # Table widget demo

docs/ROADMAP.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ This file is a forward-only todo list for professional-grade `v-gui`.
1919

2020
### Input + Forms
2121

22-
- [ ] Input masking engine (`9`, `a`, `*`, literals, escaped tokens, custom tokens)
22+
- [x] Input masking engine (`9`, `a`, `*`, literals, escaped tokens, custom tokens)
2323
- [x] Ready masks: phone, date, time, credit card, postal, SSN
24-
- [ ] Numeric input view with locale-aware parse/format and step controls
24+
- [x] Numeric input view with locale-aware parse/format and step controls
2525
- [ ] Currency/percent input modes with round-trip-safe formatting
2626
- [ ] Form validation model: sync/async validators, touched/dirty state, error slots
2727
- [ ] Unified input formatter pipeline (pre-commit transform + post-commit normalize)

examples/numeric_input.v

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import gui
2+
3+
@[heap]
4+
struct NumericInputApp {
5+
mut:
6+
en_text string = '1,234.50'
7+
en_value ?f64 = 1234.5
8+
de_text string = '1.234,50'
9+
de_value ?f64 = 1234.5
10+
}
11+
12+
fn main() {
13+
mut window := gui.window(
14+
title: 'Numeric Input'
15+
state: &NumericInputApp{}
16+
width: 300
17+
height: 280
18+
on_init: fn (mut w gui.Window) {
19+
w.update_view(main_view)
20+
w.set_id_focus(1)
21+
}
22+
)
23+
window.set_theme(gui.theme_dark_bordered)
24+
window.run()
25+
}
26+
27+
fn main_view(window &gui.Window) gui.View {
28+
w, h := window.window_size()
29+
app := window.state[NumericInputApp]()
30+
return gui.column(
31+
width: w
32+
height: h
33+
sizing: gui.fixed_fixed
34+
padding: gui.padding_medium
35+
spacing: gui.spacing_medium
36+
content: [
37+
gui.text(text: 'Locale: en_US'),
38+
gui.numeric_input(
39+
id: 'num_en'
40+
id_focus: 1
41+
text: app.en_text
42+
value: app.en_value
43+
decimals: 2
44+
min: 0.0
45+
max: 10000.0
46+
size_border: 1.5
47+
color_border: gui.rgb(160, 160, 160)
48+
color_border_focus: gui.rgb(81, 165, 255)
49+
padding: gui.padding_two_four
50+
width: 220
51+
sizing: gui.fixed_fit
52+
on_text_changed: fn (_ &gui.Layout, text string, mut w gui.Window) {
53+
mut state := w.state[NumericInputApp]()
54+
state.en_text = text
55+
}
56+
on_value_commit: fn (_ &gui.Layout, value ?f64, text string, mut w gui.Window) {
57+
mut state := w.state[NumericInputApp]()
58+
state.en_value = value
59+
state.en_text = text
60+
}
61+
),
62+
gui.text(text: 'Committed value: ${numeric_value_text(app.en_value)}'),
63+
gui.text(text: 'Locale: de_DE'),
64+
gui.numeric_input(
65+
id: 'num_de'
66+
id_focus: 2
67+
text: app.de_text
68+
value: app.de_value
69+
decimals: 2
70+
locale: gui.NumericLocaleCfg{
71+
decimal_sep: `,`
72+
group_sep: `.`
73+
}
74+
size_border: 1.5
75+
color_border: gui.rgb(160, 160, 160)
76+
color_border_focus: gui.rgb(81, 165, 255)
77+
padding: gui.padding_two_four
78+
width: 220
79+
sizing: gui.fixed_fit
80+
on_text_changed: fn (_ &gui.Layout, text string, mut w gui.Window) {
81+
mut state := w.state[NumericInputApp]()
82+
state.de_text = text
83+
}
84+
on_value_commit: fn (_ &gui.Layout, value ?f64, text string, mut w gui.Window) {
85+
mut state := w.state[NumericInputApp]()
86+
state.de_value = value
87+
state.de_text = text
88+
}
89+
),
90+
gui.text(text: 'Committed value: ${numeric_value_text(app.de_value)}'),
91+
]
92+
)
93+
}
94+
95+
fn numeric_value_text(value ?f64) string {
96+
if parsed := value {
97+
return '${parsed:.2f}'
98+
}
99+
return 'none'
100+
}

0 commit comments

Comments
 (0)