1package ui
2
3import (
4 vi "github.com/bnema/nefergui/internal/image"
5 "github.com/bnema/nefergui/internal/layout"
6 "image"
7 "math"
8 "strings"
9)
10
11// Image borrows a decoded Go image. Treat its pixels as immutable while passed
12// to Image; pass a new image value to change pixels. Renderer cache identity
13// requires a comparable image.Image value (for example, *image.RGBA).
14func (n Node) Image(img image.Image, options ...ContainerOption) {
15 child := n.child("image", "img", options)
16 if child.valid() && img != nil {
17 child.element.image = img
18 if w, h, err := vi.Size(img); err == nil {
19 child.element.imageSize = layout.Size{W: float64(w), H: float64(h)}
20 }
21 }
22}
23func (n Node) Icon(name string, options ...ContainerOption) {
24 child := n.child("icon", "img", options)
25 if child.valid() {
26 child.element.text = name
27 }
28}
29func (n Node) Separator(options ...ContainerOption) { n.child("separator", "separator", options) }
30func (n Node) Spacer(options ...ContainerOption) { n.child("spacer", "", options) }
31func (n Node) Radio(label, option string, selected *string, options ...ButtonOption) ChangeEvent {
32 child := n.buttonLike("radio", "radio", label, options)
33 if child.valid() && selected != nil {
34 child.element.checked = *selected == option
35 if activated(child) && *selected != option {
36 *selected = option
37 child.element.checked = true
38 return ChangeEvent{true}
39 }
40 }
41 return ChangeEvent{}
42}
43func (n Node) Slider(label string, value *float64, min, max, step float64, options ...ValueOption) ChangeEvent {
44 var common []ContainerOption
45 for _, o := range options {
46 if c, ok := o.(ContainerOption); ok {
47 common = append(common, c)
48 }
49 }
50 child := n.child("slider", "slider", common)
51 if !child.valid() {
52 return ChangeEvent{}
53 }
54 child.element.text = label
55 for _, o := range options {
56 if _, ok := o.(ContainerOption); !ok {
57 o.valueOption(child.element)
58 }
59 }
60 child.frame.compute(child.element)
61 child.element.fraction = -1
62 if value != nil && finiteRange(min, max, step) {
63 // Painted from the value after this call's changes.
64 defer func() { child.element.fraction = sliderFraction(*value, min, max) }()
65 }
66 if value == nil || child.element.disabled || !finiteRange(min, max, step) {
67 return ChangeEvent{}
68 }
69 original := *value
70 next := original
71 if math.IsNaN(next) || math.IsInf(next, 0) {
72 next = min
73 }
74 next = sliderGrid(next, min, max, step)
75 for _, event := range child.element.events {
76 if event.Kind == "slider-pointer" {
77 if event.Track.W > 0 && !math.IsNaN(event.X) && !math.IsInf(event.X, 0) {
78 fraction := math.Max(0, math.Min(1, (event.X-event.Track.X)/event.Track.W))
79 next = sliderGrid(min+fraction*(max-min), min, max, step)
80 }
81 continue
82 }
83 if event.Kind != "slider-key" {
84 continue
85 }
86 switch strings.ToLower(event.Text) {
87 case "left", "down":
88 next = sliderGrid(next-step, min, max, step)
89 case "right", "up":
90 next = sliderGrid(next+step, min, max, step)
91 case "page_up":
92 next = sliderGrid(next+10*step, min, max, step)
93 case "page_down":
94 next = sliderGrid(next-10*step, min, max, step)
95 case "home":
96 next = min
97 case "end":
98 next = sliderGrid(max, min, max, step)
99 }
100 }
101 if (math.IsNaN(original) || math.IsInf(original, 0)) || next != original {
102 *value = next
103 return ChangeEvent{true}
104 }
105 return ChangeEvent{}
106}
107
108// sliderGrid rounds to min+n*step then clamps; the final partial step is
109// intentionally not a stop, so keyboard and pointer share the same grid.
110// A relative epsilon keeps decimal steps such as 0.1 from losing their last
111// stop. When the grid is finer than float64 precision (or the span overflows),
112// the value is only clamped.
113func sliderGrid(v, min, max, step float64) float64 {
114 if v <= min {
115 return min
116 }
117 if v >= max {
118 v = max
119 }
120 q := (max - min) / step
121 if math.IsInf(q, 0) || math.IsNaN(q) || q > 1<<53 {
122 return v
123 }
124 steps := math.Round((v - min) / step)
125 limit := math.Floor(q + q*1e-12 + 1e-12)
126 steps = math.Max(0, math.Min(steps, limit))
127 return math.Min(max, min+steps*step)
128}
129
130// sliderFraction is the painted thumb position; a non-finite value sits at
131// the minimum and an empty range at the start.
132func sliderFraction(v, min, max float64) float64 {
133 if max <= min || math.IsNaN(v) || math.IsInf(v, 0) {
134 return 0
135 }
136 f := (v - min) / (max - min)
137 if span := max - min; math.IsInf(span, 0) {
138 // Only overflowing spans are halved: halving loses subnormal ranges.
139 f = (v/2 - min/2) / (max/2 - min/2)
140 }
141 if math.IsNaN(f) {
142 return 0
143 }
144 return math.Max(0, math.Min(1, f))
145}
146func finiteRange(min, max, step float64) bool {
147 return !math.IsNaN(min) && !math.IsNaN(max) && !math.IsNaN(step) && !math.IsInf(min, 0) && !math.IsInf(max, 0) && !math.IsInf(step, 0) && min <= max && step > 0
148}