1use crate::*;
2use serde_json::{json, Value};
3use std::collections::{HashMap, HashSet};
4use unicode_segmentation::UnicodeSegmentation;
5
6#[derive(Clone, Debug)]
7pub enum Input {
8 Move(f32, f32),
9 Down,
10 Up,
11 Wheel(f32),
12 Text(String),
13 Key {
14 key: String,
15 shift: bool,
16 command: bool,
17 },
18 Cancel,
19}
20#[derive(Clone)]
21pub struct LayoutItem {
22 pub node: Node,
23 pub rect: Rect,
24 pub clip: Rect,
25 pub parent: Option<usize>,
26 pub scroll_max: f32,
27}
28#[derive(Clone, Default)]
29struct Edit {
30 cursor: usize,
31 anchor: usize,
32}
33impl Edit {
34 fn range(&self) -> std::ops::Range<usize> {
35 self.cursor.min(self.anchor)..self.cursor.max(self.anchor)
36 }
37 fn clamp(&mut self, text: &str) {
38 let boundaries = boundaries(text);
39 self.cursor = *boundaries
40 .iter()
41 .rfind(|&&i| i <= self.cursor.min(text.len()))
42 .unwrap_or(&0);
43 self.anchor = *boundaries
44 .iter()
45 .rfind(|&&i| i <= self.anchor.min(text.len()))
46 .unwrap_or(&0);
47 }
48}
49fn boundaries(s: &str) -> Vec<usize> {
50 s.grapheme_indices(true)
51 .map(|(i, _)| i)
52 .chain(std::iter::once(s.len()))
53 .collect()
54}
55pub struct Ui {
57 root: Node,
58 pub items: Vec<LayoutItem>,
59 pub theme: Theme,
60 pub focus: Option<String>,
61 pub hover: Option<String>,
62 pressed: Option<String>,
63 pointer: (f32, f32),
64 edits: HashMap<String, Edit>,
65 scroll: HashMap<String, f32>,
66 viewport: Rect,
67 pub painter: Painter,
68 pub debug_bounds: bool,
69}
70impl Ui {
71 pub fn new(root: Node) -> Result<Self, String> {
72 validate(&root)?;
73 Ok(Self {
74 root,
75 items: vec![],
76 theme: Theme::default(),
77 focus: None,
78 hover: None,
79 pressed: None,
80 pointer: (-1.0, -1.0),
81 edits: HashMap::new(),
82 scroll: HashMap::new(),
83 viewport: Rect::default(),
84 painter: Painter::new(),
85 debug_bounds: false,
86 })
87 }
88 pub fn set_root(&mut self, root: Node) -> Result<(), String> {
89 validate(&root)?;
90 self.root = root;
91 self.layout();
92 let ids: HashSet<_> = self.items.iter().map(|i| i.node.id.clone()).collect();
93 self.edits.retain(|id, _| ids.contains(id));
94 self.scroll.retain(|id, _| ids.contains(id));
95 if self.focus.as_ref().is_some_and(|id| {
96 !self
97 .items
98 .iter()
99 .any(|i| &i.node.id == id && i.node.focusable())
100 }) {
101 self.focus = None;
102 }
103 if self.pressed.as_ref().is_some_and(|id| {
104 !self
105 .items
106 .iter()
107 .any(|i| &i.node.id == id && i.node.focusable())
108 }) {
109 self.pressed = None;
110 }
111 for item in &self.items {
112 if let Kind::TextInput { value, .. } = &item.node.kind {
113 if let Some(e) = self.edits.get_mut(&item.node.id) {
114 e.clamp(value);
115 }
116 }
117 }
118 Ok(())
119 }
120 pub fn resize(&mut self, width: u32, height: u32, scale: f32) {
121 self.painter.resize(width, height, scale);
122 self.viewport = Rect::new(
123 0.0,
124 0.0,
125 width as f32 / self.painter.scale,
126 height as f32 / self.painter.scale,
127 );
128 self.layout();
129 }
130 fn natural(&self, n: &Node) -> (f32, f32) {
131 let p = n.style.padding * 2.0;
132 let size = match &n.kind {
133 Kind::Container { axis, .. } => {
134 let sizes: Vec<_> = n.children.iter().map(|c| self.natural(c)).collect();
135 let gap = n.style.gap * sizes.len().saturating_sub(1) as f32;
136 if *axis == Axis::Vertical {
137 (
138 sizes.iter().map(|s| s.0).fold(0.0, f32::max) + p,
139 sizes.iter().map(|s| s.1).sum::<f32>() + gap + p,
140 )
141 } else {
142 (
143 sizes.iter().map(|s| s.0).sum::<f32>() + gap + p,
144 sizes.iter().map(|s| s.1).fold(0.0, f32::max) + p,
145 )
146 }
147 }
148 Kind::Label => (
149 self.painter
150 .measure(&n.label, n.style.text_size, n.style.bold)
151 + p,
152 n.style.text_size * 1.5 + p,
153 ),
154 _ => (
155 self.painter
156 .measure(&n.label, n.style.text_size, n.style.bold)
157 + 40.0,
158 42.0,
159 ),
160 };
161 (
162 if let Length::Px(w) = n.style.width {
163 w
164 } else {
165 size.0
166 },
167 if let Length::Px(h) = n.style.height {
168 h
169 } else {
170 size.1
171 },
172 )
173 }
174 pub fn layout(&mut self) {
175 self.items.clear();
176 self.place(self.root.clone(), self.viewport, self.viewport, None, true);
177 }
178 fn place(&mut self, mut n: Node, r: Rect, clip: Rect, parent: Option<usize>, enabled: bool) {
179 n.enabled &= enabled;
180 let idx = self.items.len();
181 let ownclip = r.intersect(clip);
182 let inner = r.inset(n.style.padding);
183 self.items.push(LayoutItem {
184 node: n.clone(),
185 rect: r,
186 clip: ownclip,
187 parent,
188 scroll_max: 0.0,
189 });
190 if let Kind::Container { axis, scroll } = &n.kind {
191 let vertical = *axis == Axis::Vertical;
192 let available = if vertical { inner.h } else { inner.w };
193 let natural: Vec<_> = n.children.iter().map(|c| self.natural(c)).collect();
194 let fills = n
195 .children
196 .iter()
197 .filter(|c| {
198 matches!(
199 if vertical {
200 c.style.height
201 } else {
202 c.style.width
203 },
204 Length::Fill
205 )
206 })
207 .count();
208 let fixed: f32 = n
209 .children
210 .iter()
211 .zip(&natural)
212 .filter(|(c, _)| {
213 !matches!(
214 if vertical {
215 c.style.height
216 } else {
217 c.style.width
218 },
219 Length::Fill
220 )
221 })
222 .map(|(_, s)| if vertical { s.1 } else { s.0 })
223 .sum();
224 let gaps = n.style.gap * n.children.len().saturating_sub(1) as f32;
225 let flex = (available - fixed - gaps).max(0.0) / fills.max(1) as f32;
226 let total = fixed + gaps + flex * fills as f32;
227 let maxscroll = if *scroll {
228 (total - available).max(0.0)
229 } else {
230 0.0
231 };
232 let offset = if *scroll {
233 let o = self.scroll.entry(n.id.clone()).or_default();
234 *o = o.clamp(0.0, maxscroll);
235 *o
236 } else {
237 0.0
238 };
239 self.items[idx].scroll_max = maxscroll;
240 let mut pos = if vertical { inner.y } else { inner.x } - offset;
241 for (c, s) in n.children.iter().zip(natural) {
242 let main = if matches!(
243 if vertical {
244 c.style.height
245 } else {
246 c.style.width
247 },
248 Length::Fill
249 ) {
250 flex
251 } else if vertical {
252 s.1
253 } else {
254 s.0
255 };
256 let cross = if vertical {
257 match c.style.width {
258 Length::Fill => inner.w,
259 Length::Px(v) => v,
260 Length::Auto => s.0,
261 }
262 } else {
263 match c.style.height {
264 Length::Fill => inner.h,
265 Length::Px(v) => v,
266 Length::Auto => s.1,
267 }
268 };
269 let cr = if vertical {
270 Rect::new(inner.x, pos, cross, main)
271 } else {
272 Rect::new(pos, inner.y, main, cross)
273 };
274 self.place(
275 c.clone(),
276 cr,
277 ownclip.intersect(inner),
278 Some(idx),
279 n.enabled,
280 );
281 pos += main + n.style.gap;
282 }
283 }
284 }
285 fn hit(&self) -> Option<usize> {
286 self.items
287 .iter()
288 .rposition(|i| i.node.focusable() && i.clip.contains(self.pointer.0, self.pointer.1))
289 }
290 pub fn item(&self, id: &str) -> Option<&LayoutItem> {
291 self.items.iter().find(|i| i.node.id == id)
292 }
293 fn activate(n: &Node) -> Vec<Action> {
294 let kind = match &n.kind {
295 Kind::Button
296 | Kind::Custom {
297 interactive: true, ..
298 } => ActionKind::Activate,
299 Kind::Toggle(v) => ActionKind::Toggle(!v),
300 _ => return vec![],
301 };
302 vec![Action {
303 id: n.id.clone(),
304 kind,
305 }]
306 }
307 fn slider(n: &Node, v: f32) -> Vec<Action> {
308 if let Kind::Slider { min, max, step, .. } = n.kind {
309 let value = (min + ((v - min) / step).round() * step).clamp(min, max);
310 vec![Action {
311 id: n.id.clone(),
312 kind: ActionKind::ChangeNumber(value),
313 }]
314 } else {
315 vec![]
316 }
317 }
318 fn drag(&self, i: &LayoutItem) -> Vec<Action> {
319 if let Kind::Slider { min, max, .. } = i.node.kind {
320 let t =
321 ((self.pointer.0 - i.rect.x - 10.0) / (i.rect.w - 20.0).max(1.0)).clamp(0.0, 1.0);
322 Self::slider(&i.node, min + t * (max - min))
323 } else {
324 vec![]
325 }
326 }
327 fn set_focus(&mut self, id: Option<String>) {
328 self.focus = id;
329 if let Some(id) = self.focus.clone() {
330 if let Some(item) = self.item(&id) {
331 if let Kind::TextInput { value, .. } = &item.node.kind {
332 let len = value.len();
333 self.edits.entry(id.clone()).or_insert(Edit {
334 cursor: len,
335 anchor: len,
336 });
337 }
338 }
339 if let Some(index) = self.items.iter().position(|i| i.node.id == id) {
340 let r = self.items[index].rect;
341 let mut parent = self.items[index].parent;
342 while let Some(p) = parent {
343 let i = &self.items[p];
344 if matches!(i.node.kind, Kind::Container { scroll: true, .. }) {
345 let inner = i.rect.inset(i.node.style.padding);
346 let delta = if r.y < inner.y {
347 r.y - inner.y
348 } else if r.y + r.h > inner.y + inner.h {
349 r.y + r.h - inner.y - inner.h
350 } else {
351 0.0
352 };
353 *self.scroll.entry(i.node.id.clone()).or_default() += delta;
354 }
355 parent = i.parent;
356 }
357 self.layout();
358 }
359 }
360 }
361 fn pointer_edit(&mut self, item: &LayoutItem, extend: bool) -> bool {
362 let Kind::TextInput { value, .. } = &item.node.kind else {
363 return false;
364 };
365 let current = self.edits.get(&item.node.id).cloned().unwrap_or(Edit {
366 cursor: value.len(),
367 anchor: value.len(),
368 });
369 let size = item.node.style.text_size;
370 let caret = self.painter.measure(&value[..current.cursor], size, false);
371 let offset = (caret - (item.rect.w - 20.0) + 2.0).max(0.0);
372 let target = self.pointer.0 - item.rect.x - 10.0 + offset;
373 let index = boundaries(value)
374 .into_iter()
375 .min_by(|a, b| {
376 let da = (self.painter.measure(&value[..*a], size, false) - target).abs();
377 let db = (self.painter.measure(&value[..*b], size, false) - target).abs();
378 da.total_cmp(&db)
379 })
380 .unwrap_or(0);
381 let edit = self.edits.entry(item.node.id.clone()).or_default();
382 edit.cursor = index;
383 if !extend {
384 edit.anchor = index;
385 }
386 true
387 }
388 pub fn event(&mut self, input: Input) -> Vec<Action> {
389 match input {
390 Input::Move(x, y) => {
391 self.pointer = (x, y);
392 self.hover = self.hit().map(|i| self.items[i].node.id.clone());
393 if let Some(i) = self.pressed.as_ref().and_then(|id| self.item(id)).cloned() {
394 if !self.pointer_edit(&i, true) {
395 return self.drag(&i);
396 }
397 }
398 }
399 Input::Down => {
400 let hit = self.hit();
401 self.pressed = hit.map(|i| self.items[i].node.id.clone());
402 self.set_focus(self.pressed.clone());
403 if let Some(i) = self.pressed.as_ref().and_then(|id| self.item(id)).cloned() {
404 if !self.pointer_edit(&i, false) {
405 return self.drag(&i);
406 }
407 }
408 }
409 Input::Up => {
410 let pressed = self.pressed.take();
411 if let Some(i) = self.hit() {
412 if pressed.as_ref() == Some(&self.items[i].node.id) {
413 return Self::activate(&self.items[i].node);
414 }
415 }
416 }
417 Input::Cancel => {
418 self.pressed = None;
419 self.hover = None;
420 }
421 Input::Wheel(delta) => {
422 if !delta.is_finite() {
423 return vec![];
424 }
425 if let Some(i) = self.items.iter().rposition(|i| {
426 i.clip.contains(self.pointer.0, self.pointer.1)
427 && matches!(i.node.kind, Kind::Container { scroll: true, .. })
428 }) {
429 let item = &self.items[i];
430 let o = self.scroll.entry(item.node.id.clone()).or_default();
431 *o = (*o + delta).clamp(0.0, item.scroll_max);
432 self.layout();
433 }
434 }
435 Input::Text(text) => return self.edit_text(Some(text), "", false, false),
436 Input::Key {
437 key,
438 shift,
439 command,
440 } => {
441 if key == "Tab" {
442 let ids: Vec<_> = self
443 .items
444 .iter()
445 .filter(|i| i.node.focusable())
446 .map(|i| i.node.id.clone())
447 .collect();
448 if !ids.is_empty() {
449 let current = self
450 .focus
451 .as_ref()
452 .and_then(|id| ids.iter().position(|v| v == id));
453 let next = match current {
454 None => {
455 if shift {
456 ids.len() - 1
457 } else {
458 0
459 }
460 }
461 Some(i) => {
462 if shift {
463 (i + ids.len() - 1) % ids.len()
464 } else {
465 (i + 1) % ids.len()
466 }
467 }
468 };
469 self.set_focus(Some(ids[next].clone()));
470 }
471 } else if let Some(n) = self
472 .focus
473 .as_ref()
474 .and_then(|id| self.item(id))
475 .map(|i| i.node.clone())
476 {
477 match &n.kind {
478 Kind::TextInput { .. } => {
479 return self.edit_text(None, &key, shift, command)
480 }
481 Kind::Slider {
482 value,
483 min,
484 max,
485 step,
486 } => {
487 let v = match key.as_str() {
488 "ArrowLeft" | "ArrowDown" => value - step,
489 "ArrowRight" | "ArrowUp" => value + step,
490 "Home" => *min,
491 "End" => *max,
492 _ => return vec![],
493 };
494 return Self::slider(&n, v);
495 }
496 _ => {
497 if key == "Enter" || key == "Space" {
498 return Self::activate(&n);
499 }
500 }
501 }
502 }
503 }
504 }
505 vec![]
506 }
507 fn edit_text(
508 &mut self,
509 text: Option<String>,
510 key: &str,
511 shift: bool,
512 command: bool,
513 ) -> Vec<Action> {
514 let Some(id) = self.focus.clone() else {
515 return vec![];
516 };
517 let Some(item) = self.item(&id) else {
518 return vec![];
519 };
520 let Kind::TextInput { value, .. } = &item.node.kind else {
521 return vec![];
522 };
523 let mut value = value.clone();
524 let e = self.edits.entry(id.clone()).or_insert(Edit {
525 cursor: value.len(),
526 anchor: value.len(),
527 });
528 e.clamp(&value);
529 let original = value.clone();
530 let points = boundaries(&value);
531 let prev = *points.iter().rfind(|&&p| p < e.cursor).unwrap_or(&0);
532 let next = *points
533 .iter()
534 .find(|&&p| p > e.cursor)
535 .unwrap_or(&value.len());
536 if let Some(text) = text {
537 let text: String = text.chars().filter(|c| !c.is_control()).collect();
538 let range = e.range();
539 let candidate_len = value.len() - range.len() + text.len();
540 if candidate_len <= 16384 {
541 let start = range.start;
542 value.replace_range(range, &text);
543 e.cursor = start + text.len();
544 e.anchor = e.cursor;
545 }
546 } else {
547 match key {
548 "a" | "A" if command => {
549 e.anchor = 0;
550 e.cursor = value.len();
551 }
552 "ArrowLeft" => {
553 e.cursor = if !shift && e.cursor != e.anchor {
554 e.range().start
555 } else {
556 prev
557 };
558 if !shift {
559 e.anchor = e.cursor;
560 }
561 }
562 "ArrowRight" => {
563 e.cursor = if !shift && e.cursor != e.anchor {
564 e.range().end
565 } else {
566 next
567 };
568 if !shift {
569 e.anchor = e.cursor;
570 }
571 }
572 "Home" => {
573 e.cursor = 0;
574 if !shift {
575 e.anchor = 0;
576 }
577 }
578 "End" => {
579 e.cursor = value.len();
580 if !shift {
581 e.anchor = e.cursor;
582 }
583 }
584 "Backspace" | "Delete" => {
585 let range = if e.cursor != e.anchor {
586 e.range()
587 } else if key == "Backspace" {
588 prev..e.cursor
589 } else {
590 e.cursor..next
591 };
592 e.cursor = range.start;
593 e.anchor = e.cursor;
594 value.replace_range(range, "");
595 }
596 "Enter" => {
597 return vec![Action {
598 id,
599 kind: ActionKind::Submit(value),
600 }]
601 }
602 _ => {}
603 }
604 }
605 if value != original {
606 vec![Action {
607 id,
608 kind: ActionKind::ChangeText(value),
609 }]
610 } else {
611 vec![]
612 }
613 }
614 pub fn agent_action(
616 &mut self,
617 op: &str,
618 id: &str,
619 text: Option<&str>,
620 value: Option<f32>,
621 ) -> Result<Vec<Action>, String> {
622 let item = self
623 .item(id)
624 .ok_or_else(|| format!("unknown widget: {id}"))?
625 .clone();
626 if !item.node.focusable() {
627 return Err(format!("widget is disabled or not actionable: {id}"));
628 }
629 match op {
630 "click"
631 if matches!(
632 item.node.kind,
633 Kind::Button
634 | Kind::Toggle(_)
635 | Kind::Custom {
636 interactive: true,
637 ..
638 }
639 ) =>
640 {
641 self.set_focus(Some(id.into()));
642 Ok(Self::activate(&item.node))
643 }
644 "set_text" if matches!(item.node.kind, Kind::TextInput { .. }) => {
645 let text = text.ok_or("text is required")?;
646 if text.len() > 16384 || text.chars().any(char::is_control) {
647 return Err("text must be a single line, at most 16384 bytes".into());
648 }
649 self.set_focus(Some(id.into()));
650 self.edit_text(None, "a", false, true);
651 Ok(self.edit_text(Some(text.into()), "", false, false))
652 }
653 "set_value" => {
654 let v = value.ok_or("value is required")?;
655 if let Kind::Slider { min, max, .. } = item.node.kind {
656 if !v.is_finite() || v < min || v > max {
657 return Err("value outside slider range".into());
658 }
659 self.set_focus(Some(id.into()));
660 Ok(Self::slider(&item.node, v))
661 } else {
662 Err("set_value requires a slider".into())
663 }
664 }
665 _ => Err(format!("{op} is not supported by {}", item.node.role())),
666 }
667 }
668 pub fn inspect(&self) -> Value {
669 json!({"protocol":"forge-ui/1","viewport":self.viewport,"scale":self.painter.scale,"focus":self.focus,"nodes":self.items.iter().map(|i|{
670 let n=&i.node;let (value,range)=match &n.kind{Kind::Toggle(v)=>(json!(v),Value::Null),Kind::Slider{value,min,max,step}=>(json!(value),json!({"min":min,"max":max,"step":step})),Kind::TextInput{value,..}=>(json!(value),Value::Null),_=>(Value::Null,Value::Null)};
671 let actions=if !n.focusable(){vec![]}else{match n.kind{Kind::TextInput{..}=>vec!["set_text","key"],Kind::Slider{..}=>vec!["set_value","key"],_=>vec!["click","key"]}};
672 json!({"id":n.id,"parent":i.parent.map(|p|self.items[p].node.id.as_str()),"role":n.role(),"label":n.label,"value":value,"range":range,"bounds":i.rect,"visible_bounds":i.clip,"visible":i.clip.w>0.0&&i.clip.h>0.0,"enabled":n.enabled,"focused":self.focus.as_ref()==Some(&n.id),"actions":actions,"scroll_offset":self.scroll.get(&n.id).copied().unwrap_or(0.0),"scroll_max":i.scroll_max})
673 }).collect::<Vec<_>>()})
674 }
675 pub fn paint(&mut self) {
676 self.painter.clear(self.theme.background);
677 let t = self.theme;
678 for item in &self.items {
679 let n = &item.node;
680 let r = item.rect;
681 if item.clip.w <= 0.0 || item.clip.h <= 0.0 {
682 continue;
683 }
684 self.painter.set_clip(item.clip);
685 let p = &mut self.painter;
686 let hovered = self.hover.as_ref() == Some(&n.id);
687 let focused = self.focus.as_ref() == Some(&n.id);
688 let down = self.pressed.as_ref() == Some(&n.id);
689 let fg = if n.enabled {
690 n.style.foreground.unwrap_or(t.text)
691 } else {
692 t.muted.mix(t.panel, 0.45)
693 };
694 if let Some(bg) = n.style.background {
695 p.rect(r, bg, n.style.radius);
696 }
697 if n.style.border {
698 p.outline(r, t.border);
699 }
700 match &n.kind {
701 Kind::Container { scroll, .. } => {
702 if *scroll && item.scroll_max > 0.0 {
703 let inner = r.inset(n.style.padding);
704 let h = (inner.h * inner.h / (inner.h + item.scroll_max)).max(18.0);
705 let offset = self.scroll.get(&n.id).copied().unwrap_or(0.0);
706 p.rect(
707 Rect::new(
708 r.x + r.w - 4.0,
709 inner.y + offset / item.scroll_max * (inner.h - h),
710 3.0,
711 h,
712 ),
713 t.border,
714 1.0,
715 );
716 }
717 }
718 Kind::Label => p.text(
719 &n.label,
720 r.x + n.style.padding,
721 r.y + n.style.padding,
722 n.style.text_size,
723 fg,
724 n.style.bold,
725 ),
726 Kind::Button => {
727 let bg =
728 n.style
729 .background
730 .unwrap_or(if n.enabled { t.elevated } else { t.panel });
731 p.rect(
732 r,
733 if down {
734 bg.mix(t.accent, 0.35)
735 } else if hovered {
736 bg.mix(t.text, 0.09)
737 } else {
738 bg
739 },
740 n.style.radius,
741 );
742 let w = p.measure(&n.label, n.style.text_size, n.style.bold);
743 p.text(
744 &n.label,
745 r.x + (r.w - w) / 2.0,
746 r.y + (r.h - n.style.text_size * 1.3) / 2.0,
747 n.style.text_size,
748 fg,
749 n.style.bold,
750 );
751 }
752 Kind::Toggle(v) => {
753 p.text(&n.label, r.x, r.y + 9.0, n.style.text_size, fg, false);
754 let x = r.x + r.w - 46.0;
755 p.rect(
756 Rect::new(x, r.y + 8.0, 44.0, 24.0),
757 if *v { t.accent } else { t.border },
758 12.0,
759 );
760 p.rect(
761 Rect::new(x + if *v { 23.0 } else { 3.0 }, r.y + 11.0, 18.0, 18.0),
762 if *v { t.on_accent } else { t.text },
763 9.0,
764 );
765 }
766 Kind::Slider {
767 value, min, max, ..
768 } => {
769 p.text(&n.label, r.x, r.y, n.style.text_size, fg, false);
770 let s = format!("{value:.0}");
771 let w = p.measure(&s, 13.0, true);
772 p.text(&s, r.x + r.w - w, r.y, 13.0, t.accent, true);
773 let x = r.x + 10.0;
774 let width = (r.w - 20.0).max(0.0);
775 let frac = (value - min) / (max - min);
776 p.rect(Rect::new(x, r.y + 37.0, width, 3.0), t.border, 1.0);
777 p.rect(Rect::new(x, r.y + 37.0, width * frac, 3.0), t.accent, 1.0);
778 p.rect(
779 Rect::new(x + width * frac - 6.0, r.y + 32.0, 12.0, 12.0),
780 t.accent,
781 3.0,
782 );
783 }
784 Kind::TextInput { value, placeholder } => {
785 p.rect(r, t.background, n.style.radius);
786 p.outline(r, if focused { t.accent } else { t.border });
787 let area = r.inset(10.0);
788 p.set_clip(item.clip.intersect(area));
789 let e = self.edits.get(&n.id).cloned().unwrap_or(Edit {
790 cursor: value.len(),
791 anchor: value.len(),
792 });
793 let caret = p.measure(
794 &value[..e.cursor.min(value.len())],
795 n.style.text_size,
796 false,
797 );
798 let off = if focused {
799 (caret - area.w + 2.0).max(0.0)
800 } else {
801 0.0
802 };
803 if focused && e.cursor != e.anchor {
804 let sel = e.range();
805 let a = p.measure(&value[..sel.start], n.style.text_size, false);
806 let b = p.measure(&value[..sel.end], n.style.text_size, false);
807 p.rect(
808 Rect::new(area.x + a - off, r.y + 9.0, b - a, 24.0),
809 t.accent.mix(t.background, 0.7),
810 0.0,
811 );
812 }
813 p.text(
814 if value.is_empty() { placeholder } else { value },
815 area.x - off,
816 r.y + 10.0,
817 n.style.text_size,
818 if value.is_empty() { t.muted } else { fg },
819 false,
820 );
821 if focused {
822 p.rect(
823 Rect::new(area.x + caret - off, r.y + 10.0, 1.0, 22.0),
824 t.accent,
825 0.0,
826 );
827 }
828 p.set_clip(item.clip);
829 }
830 Kind::Custom { paint, .. } => paint(p, r, &t),
831 }
832 p.set_clip(item.clip);
833 if focused {
834 p.outline(r.inset(1.0), t.accent);
835 }
836 if self.debug_bounds {
837 p.outline(r, t.success);
838 }
839 }
840 }
841}
842fn validate(root: &Node) -> Result<(), String> {
843 fn visit(n: &Node, ids: &mut HashSet<String>) -> Result<(), String> {
844 if n.id.is_empty() || !ids.insert(n.id.clone()) {
845 return Err(format!("empty or duplicate widget ID: {}", n.id));
846 }
847 let s = &n.style;
848 let mut numbers = vec![s.padding, s.gap, s.radius, s.text_size];
849 if let Length::Px(v) = s.width {
850 numbers.push(v);
851 }
852 if let Length::Px(v) = s.height {
853 numbers.push(v);
854 }
855 if numbers.iter().any(|v| !v.is_finite() || *v < 0.0) || s.text_size > 256.0 {
856 return Err(format!("invalid style: {}", n.id));
857 }
858 if let Kind::Slider {
859 value,
860 min,
861 max,
862 step,
863 } = &n.kind
864 {
865 if [value, min, max, step].iter().any(|v| !v.is_finite())
866 || min >= max
867 || *step <= 0.0
868 || value < min
869 || value > max
870 {
871 return Err(format!("invalid slider: {}", n.id));
872 }
873 }
874 if let Kind::TextInput { value, .. } = &n.kind {
875 if value.len() > 16384 || value.chars().any(char::is_control) {
876 return Err(format!("invalid single-line text: {}", n.id));
877 }
878 }
879 for c in &n.children {
880 visit(c, ids)?;
881 }
882 Ok(())
883 }
884 visit(root, &mut HashSet::new())
885}