Skip to main content

forge_ui/
node.rs

1use crate::{Painter, Rect};
2use serde::Serialize;
3use std::sync::Arc;
4
5/// Packed opaque RGB color, independent of the windowing backend.
6#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
7pub struct Color(pub u32);
8impl Color {
9    pub const fn rgb(rgb: u32) -> Self {
10        Self(rgb & 0xffffff)
11    }
12    pub fn mix(self, other: Self, amount: f32) -> Self {
13        let t = amount.clamp(0.0, 1.0);
14        let channel = |shift: u32| {
15            (((self.0 >> shift) & 255) as f32 * (1.0 - t) + ((other.0 >> shift) & 255) as f32 * t)
16                as u32
17        };
18        Self(channel(16) << 16 | channel(8) << 8 | channel(0))
19    }
20}
21/// Shared design tokens. All widget colors are derived from these tokens.
22#[derive(Clone, Copy, Debug)]
23pub struct Theme {
24    pub background: Color,
25    pub panel: Color,
26    pub elevated: Color,
27    pub border: Color,
28    pub text: Color,
29    pub muted: Color,
30    pub accent: Color,
31    pub on_accent: Color,
32    pub success: Color,
33}
34impl Theme {
35    pub const DARK: Self = Self {
36        background: Color(0x101316),
37        panel: Color(0x191d21),
38        elevated: Color(0x242a30),
39        border: Color(0x343c43),
40        text: Color(0xf2f3ef),
41        muted: Color(0x9ca8b2),
42        accent: Color(0xff6548),
43        on_accent: Color(0x17120f),
44        success: Color(0x8cdbb6),
45    };
46    pub const LIGHT: Self = Self {
47        background: Color(0xf1f2ef),
48        panel: Color(0xffffff),
49        elevated: Color(0xe6e9e6),
50        border: Color(0xc8ceca),
51        text: Color(0x182025),
52        muted: Color(0x56636c),
53        accent: Color(0xc73e26),
54        on_accent: Color(0xffffff),
55        success: Color(0x19774e),
56    };
57}
58impl Default for Theme {
59    fn default() -> Self {
60        Self::DARK
61    }
62}
63#[derive(Clone, Copy, Debug, Default)]
64pub enum Length {
65    #[default]
66    Auto,
67    Px(f32),
68    Fill,
69}
70#[derive(Clone, Copy, Debug, PartialEq)]
71pub enum Axis {
72    Horizontal,
73    Vertical,
74}
75#[derive(Clone, Debug)]
76pub struct Style {
77    pub width: Length,
78    pub height: Length,
79    pub padding: f32,
80    pub gap: f32,
81    pub background: Option<Color>,
82    pub foreground: Option<Color>,
83    pub border: bool,
84    pub radius: f32,
85    pub text_size: f32,
86    pub bold: bool,
87}
88impl Default for Style {
89    fn default() -> Self {
90        Self {
91            width: Length::Fill,
92            height: Length::Auto,
93            padding: 0.0,
94            gap: 0.0,
95            background: None,
96            foreground: None,
97            border: false,
98            radius: 6.0,
99            text_size: 14.0,
100            bold: false,
101        }
102    }
103}
104/// Custom painting is clipped to this node's rectangle. It never receives window access.
105pub type PaintFn = Arc<dyn Fn(&mut Painter, Rect, &Theme) + Send + Sync>;
106#[derive(Clone)]
107pub enum Kind {
108    Container {
109        axis: Axis,
110        scroll: bool,
111    },
112    Label,
113    Button,
114    Toggle(bool),
115    Slider {
116        value: f32,
117        min: f32,
118        max: f32,
119        step: f32,
120    },
121    TextInput {
122        value: String,
123        placeholder: String,
124    },
125    Custom {
126        paint: PaintFn,
127        interactive: bool,
128    },
129}
130/// Declarative widget tree; retained interaction state lives in `Ui`, keyed by `id`.
131#[derive(Clone)]
132pub struct Node {
133    pub id: String,
134    pub label: String,
135    pub kind: Kind,
136    pub style: Style,
137    pub enabled: bool,
138    pub children: Vec<Node>,
139}
140impl Node {
141    fn new(id: impl Into<String>, label: impl Into<String>, kind: Kind) -> Self {
142        Self {
143            id: id.into(),
144            label: label.into(),
145            kind,
146            style: Style::default(),
147            enabled: true,
148            children: vec![],
149        }
150    }
151    pub fn column(id: impl Into<String>, children: Vec<Self>) -> Self {
152        Self::container(id, Axis::Vertical, children)
153    }
154    pub fn row(id: impl Into<String>, children: Vec<Self>) -> Self {
155        Self::container(id, Axis::Horizontal, children)
156    }
157    fn container(id: impl Into<String>, axis: Axis, children: Vec<Self>) -> Self {
158        let mut n = Self::new(
159            id,
160            "",
161            Kind::Container {
162                axis,
163                scroll: false,
164            },
165        );
166        n.children = children;
167        n
168    }
169    /// Vertical scrolling viewport. Give it a fixed height or `fill_height`.
170    pub fn scroll(id: impl Into<String>, children: Vec<Self>) -> Self {
171        let mut n = Self::column(id, children);
172        n.kind = Kind::Container {
173            axis: Axis::Vertical,
174            scroll: true,
175        };
176        n
177    }
178    pub fn label(id: impl Into<String>, text: impl Into<String>) -> Self {
179        Self::new(id, text, Kind::Label)
180    }
181    pub fn button(id: impl Into<String>, text: impl Into<String>) -> Self {
182        Self::new(id, text, Kind::Button).height(40.0)
183    }
184    pub fn toggle(id: impl Into<String>, label: impl Into<String>, checked: bool) -> Self {
185        Self::new(id, label, Kind::Toggle(checked)).height(40.0)
186    }
187    /// Invalid ranges/values are rejected by `Ui::set_root`, not silently accepted.
188    pub fn slider(
189        id: impl Into<String>,
190        label: impl Into<String>,
191        value: f32,
192        min: f32,
193        max: f32,
194        step: f32,
195    ) -> Self {
196        Self::new(
197            id,
198            label,
199            Kind::Slider {
200                value,
201                min,
202                max,
203                step,
204            },
205        )
206        .height(54.0)
207    }
208    pub fn text_input(
209        id: impl Into<String>,
210        label: impl Into<String>,
211        value: impl Into<String>,
212        placeholder: impl Into<String>,
213    ) -> Self {
214        Self::new(
215            id,
216            label,
217            Kind::TextInput {
218                value: value.into(),
219                placeholder: placeholder.into(),
220            },
221        )
222        .height(42.0)
223    }
224    pub fn custom(
225        id: impl Into<String>,
226        label: impl Into<String>,
227        interactive: bool,
228        paint: impl Fn(&mut Painter, Rect, &Theme) + Send + Sync + 'static,
229    ) -> Self {
230        Self::new(
231            id,
232            label,
233            Kind::Custom {
234                paint: Arc::new(paint),
235                interactive,
236            },
237        )
238        .height(120.0)
239    }
240    pub fn width(mut self, px: f32) -> Self {
241        self.style.width = Length::Px(px);
242        self
243    }
244    pub fn height(mut self, px: f32) -> Self {
245        self.style.height = Length::Px(px);
246        self
247    }
248    pub fn auto_width(mut self) -> Self {
249        self.style.width = Length::Auto;
250        self
251    }
252    pub fn fill_height(mut self) -> Self {
253        self.style.height = Length::Fill;
254        self
255    }
256    pub fn padding(mut self, px: f32) -> Self {
257        self.style.padding = px;
258        self
259    }
260    pub fn gap(mut self, px: f32) -> Self {
261        self.style.gap = px;
262        self
263    }
264    pub fn background(mut self, color: Color) -> Self {
265        self.style.background = Some(color);
266        self
267    }
268    pub fn color(mut self, color: Color) -> Self {
269        self.style.foreground = Some(color);
270        self
271    }
272    pub fn border(mut self) -> Self {
273        self.style.border = true;
274        self
275    }
276    pub fn radius(mut self, px: f32) -> Self {
277        self.style.radius = px;
278        self
279    }
280    pub fn font_size(mut self, px: f32) -> Self {
281        self.style.text_size = px;
282        self
283    }
284    pub fn bold(mut self) -> Self {
285        self.style.bold = true;
286        self
287    }
288    pub fn disabled(mut self) -> Self {
289        self.enabled = false;
290        self
291    }
292    pub fn focusable(&self) -> bool {
293        self.enabled
294            && matches!(
295                self.kind,
296                Kind::Button
297                    | Kind::Toggle(_)
298                    | Kind::Slider { .. }
299                    | Kind::TextInput { .. }
300                    | Kind::Custom {
301                        interactive: true,
302                        ..
303                    }
304            )
305    }
306    pub fn role(&self) -> &'static str {
307        match self.kind {
308            Kind::Container { scroll: true, .. } => "scroll",
309            Kind::Container { .. } => "group",
310            Kind::Label => "label",
311            Kind::Button => "button",
312            Kind::Toggle(_) => "switch",
313            Kind::Slider { .. } => "slider",
314            Kind::TextInput { .. } => "textbox",
315            Kind::Custom { .. } => "custom",
316        }
317    }
318}
319/// Semantic result of a human or agent interaction, reduced by the application.
320#[derive(Clone, Debug, Serialize, PartialEq)]
321pub struct Action {
322    pub id: String,
323    pub kind: ActionKind,
324}
325#[derive(Clone, Debug, Serialize, PartialEq)]
326#[serde(tag = "type", content = "value", rename_all = "snake_case")]
327pub enum ActionKind {
328    Activate,
329    Toggle(bool),
330    ChangeNumber(f32),
331    ChangeText(String),
332    Submit(String),
333}