pane_ui 0.1.0

A RON-driven, hot-reloadable wgpu UI library with spring animations and consistent scaling
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
//! UI item definitions.
//!
//! Each struct here is a fully-described, renderable widget. Items are created by
//! the builder from RON definitions and stored in [`Root`](crate::logic::Root) roots.
//! They carry no render state — all mutable frame state lives in the parallel
//! `ItemState` vec in `logic.rs`.
//!
//! ## Container children
//!
//! All container children store `Vec<UiItem>` directly. There are no separate
//! `ScrollItem` or `BarItem` enums. `UiItem::Spacer` is only meaningful inside a `Bar`.

#![allow(dead_code)]
use crate::loader::{
    DropdownAction, PressAction, RadioAction, SliderAction, TextBoxAction, ToggleAction,
};
use crate::styles::StyleId;

// ── Button ────────────────────────────────────────────────────────────────────

/// A clickable button. All interactive elements — menu items, icon buttons, close
/// buttons — are represented as `Button`. Position and size are always explicit;
/// no automatic layout is applied at this level.
pub struct Button {
    pub id: String,
    pub x: f32,
    pub y: f32,
    pub width: f32,
    pub height: f32,
    /// Text rendered centred inside the button. Empty string for icon-only buttons.
    pub text: String,
    pub style: StyleId,
    /// Tooltip shown on hover. `None` for no tooltip.
    pub tooltip: Option<String>,
    /// What happens when the button is clicked.
    pub action: PressAction,
    /// When `true`, the button is drawn with its `disabled` style and ignores input.
    pub disabled: bool,
    /// If `true`, this button receives focus by default when the root is entered via gamepad.
    pub nav_default: bool,
}

// ── ScrollList ────────────────────────────────────────────────────────────────

/// A scrolling list of items, clipped to its bounds.
///
/// Items are laid out top-to-bottom (or left-to-right when `horizontal`) with `gap` spacing.
/// The list clips its children and handles scroll input (mouse wheel, touch drag) internally.
pub struct ScrollList {
    pub id: String,
    pub x: f32,
    pub y: f32,
    pub width: f32,
    pub height: f32,
    /// Inner padding applied before the first and after the last item.
    pub pad_left: f32,
    pub pad_right: f32,
    pub pad_top: f32,
    pub pad_bottom: f32,
    /// Gap between items.
    pub gap: f32,
    /// Optional background shape drawn behind the list contents.
    pub style: Option<StyleId>,
    /// If `true`, items are laid out left-to-right; height is imposed, width comes from each item.
    /// If `false` (default), items are laid out top-to-bottom; width is imposed, height from each item.
    pub horizontal: bool,
    /// If `true`, spans the full screen along the scroll axis and centers on that axis.
    pub full_span: bool,
    /// Children. In vertical mode: width imposed, height from item. In horizontal mode: height imposed, width from item.
    pub items: Vec<UiItem>,
}

// ── Bar ───────────────────────────────────────────────────────────────────────

/// Which edge of the screen a [`Bar`] is anchored to, or `Free` for manual positioning.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BarEdge {
    Top,
    Bottom,
    Left,
    Right,
    /// No edge anchor — position is set via `x`, `y`, `width`, `height` on [`Bar`].
    Free,
}

/// An edge-anchored panel that draws a background and defines layout geometry.
/// Children are first-class root items positioned by the builder at load time;
/// the Bar itself owns only the geometry needed to draw its background.
pub struct Bar {
    pub id: String,
    pub edge: BarEdge,
    /// Cross-axis size (height for top/bottom bars, width for left/right bars).
    pub thickness: f32,
    /// Padding between the bar edge and its first/last item.
    pub pad: f32,
    /// Gap between consecutive items.
    pub gap: f32,
    /// Optional background shape drawn behind bar contents.
    pub style: Option<StyleId>,
    /// Extra inset from the top of the screen (e.g. for a system status bar).
    pub top_inset: f32,
    /// Extra inset from the bottom of the screen (e.g. for a home indicator).
    pub bottom_inset: f32,
    /// Children laid out along the bar axis. Positions are computed at tick time.
    pub items: Vec<UiItem>,
    /// When true, items use their RON x/y as screen-space coordinates (same as manual `ScrollPane`).
    pub manual: bool,
    /// X position for `BarEdge::Free` bars (screen-space, origin at center).
    pub x: f32,
    /// Y position for `BarEdge::Free` bars.
    pub y: f32,
    /// Explicit width for `BarEdge::Free` bars (0.0 = use thickness).
    pub width: f32,
    /// Explicit height for `BarEdge::Free` bars (0.0 = use thickness).
    pub height: f32,
}

// ── Popout ────────────────────────────────────────────────────────────────────

/// A panel that spring-animates between a closed and open position.
///
/// The panel is toggled by a child [`Button`] whose id matches `toggle_id`. When
/// closed, the panel is at `(closed_x, closed_y)`; when open, at `(open_x, open_y)`.
/// All children are positioned relative to the panel's current animated origin.
///
/// Popouts do not clip their children — add a [`ScrollList`] child if clipping is needed.
pub struct Popout {
    pub id: String,
    /// Position when closed (centred coordinate space).
    pub closed_x: f32,
    pub closed_y: f32,
    /// Position when open (centred coordinate space).
    pub open_x: f32,
    pub open_y: f32,
    pub width: f32,
    pub height: f32,
    /// Id of the child button that toggles this popout open and closed.
    pub toggle_id: String,
    /// Optional background shape drawn behind the panel.
    pub style: Option<StyleId>,
    /// Which screen edge the popout slides in from (used to set animation direction).
    pub edge: Option<crate::items::PopoutEdge>,
    /// If `true`, a drop shadow is rendered behind the panel.
    pub shadow: bool,
    /// If `true`, children are laid out left-to-right (like a horizontal bar) with `gap` spacing.
    /// If `false` (default), children are positioned absolutely relative to the panel origin.
    pub horizontal: bool,
    /// Gap between children when `horizontal` is true.
    pub gap: f32,
    /// If `true`, stretches to fill the full screen along the cross axis of the panel's `edge`.
    /// Top/Bottom/None edge: spans full width. Left/Right edge: spans full height.
    pub full_span: bool,
    /// If `true`, pressing the controller home/guide button toggles this popout open/closed.
    pub home_toggles: bool,
    pub items: Vec<UiItem>,
}

/// The screen edge a [`Popout`] slides in from.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PopoutEdge {
    Left,
    Right,
    Top,
    Bottom,
}

// ── Toggle ────────────────────────────────────────────────────────────────────

/// A button that holds an on/off state.
///
/// Drawn with `style_off` when unchecked and `style_on` when checked. The action
/// fires with the new `bool` value on every state flip.
pub struct Toggle {
    pub id: String,
    pub x: f32,
    pub y: f32,
    pub width: f32,
    pub height: f32,
    pub text: String,
    pub tooltip: Option<String>,
    /// Initial checked value used when the toggle is first constructed.
    /// The live state lives on `ButtonState::checked`.
    pub default_checked: bool,
    pub disabled: bool,
    /// Shape used when the toggle is off (unchecked).
    pub style_off: StyleId,
    /// Shape used when the toggle is on (checked).
    pub style_on: StyleId,
    pub action: ToggleAction,
}

// ── FreeLabel ─────────────────────────────────────────────────────────────────

/// A free-standing text label positioned in the root's coordinate space.
///
/// Not interactive. Left-aligned at `(x, y)`.
pub struct FreeLabel {
    pub id: String,
    pub x: f32,
    pub y: f32,
    pub text: String,
    /// Font size in logical pixels.
    pub size: f32,
    pub color: crate::draw::Color,
    /// When used inside a Bar, how much main-axis space this label occupies.
    /// Has no effect outside of bars.
    pub width: f32,
}

// ── Divider ───────────────────────────────────────────────────────────────────

/// A solid filled rectangle used as a visual separator or decorative rule.
///
/// Visual properties (color, opacity, shader) come from the style's `idle` state.
/// Not interactive.
pub struct Divider {
    pub id: String,
    pub x: f32,
    pub y: f32,
    pub width: f32,
    pub height: f32,
    pub style: StyleId,
    /// If `true`, spans the full screen width each frame (x and width are overridden).
    pub full_span: bool,
}

// ── Image ─────────────────────────────────────────────────────────────────────

/// A non-interactive image or GIF at an explicit position.
///
/// The texture is loaded at build time. For animated GIFs the texture registry
/// advances frames automatically each render tick.
pub struct Image {
    pub id: String,
    pub x: f32,
    pub y: f32,
    pub width: f32,
    pub height: f32,
    pub texture: crate::textures::TextureId,
    pub shader: crate::draw::ShaderId,
}

// ── ProgressBar ───────────────────────────────────────────────────────────────

/// A read-only progress bar driven entirely by the application.
///
/// `value` is in `0.0..=1.0`. The track style covers the full bar width;
/// the fill style covers the filled portion. Update via
/// [`PaneOverlay::set_progress`](crate::api::PaneOverlay::set_progress).
pub struct ProgressBar {
    pub id: String,
    pub x: f32,
    pub y: f32,
    pub width: f32,
    pub height: f32,
    /// Current fill fraction in `0.0..=1.0`.
    pub value: f32,
    /// Shape for the unfilled background track.
    pub style_track: StyleId,
    /// Shape for the filled foreground portion.
    pub style_fill: StyleId,
}

// ── Actor ─────────────────────────────────────────────────────────────────────

/// A decorative, non-interactive element that animates in response to mouse events.
///
/// Actors have an origin position they rest at when idle, and a list of
/// `(trigger, action)` behaviour pairs that fire based on cursor interaction.
/// Multiple behaviours can be defined — the highest-priority active one wins.
///
/// ## Visuals
///
/// Visual priority (highest to lowest):
/// 1. A [`SwapGif`](Action::SwapGif) override from an active trigger behaviour
/// 2. The actor's base `gif`, which loops automatically at all times
/// 3. The actor's `style`, drawn as a static component
///
/// ## Z-order
///
/// - `z_front: false` — drawn in document order, interleaved with other items
/// - `z_front: true` — drawn after all other items, always on top
///
/// Actors never receive focus, block input, or emit actions.
pub struct Actor {
    pub id: String,
    /// Resting X position in centred coordinate space. The actor lerps back here when idle.
    pub origin_x: f32,
    /// Resting Y position in centred coordinate space.
    pub origin_y: f32,
    pub width: f32,
    pub height: f32,
    /// Fallback visual shape, used when no gif is set and no `SwapGif` override is active.
    pub style: Option<StyleId>,
    /// Base gif texture. Loops automatically. Shown when no `SwapGif` override is active.
    pub gif: Option<crate::textures::TextureId>,
    /// Shader used to render the base gif.
    pub gif_shader: crate::draw::ShaderId,
    /// If `true`, drawn after all other items (on top of everything).
    pub z_front: bool,
    /// If `true`, the actor lerps back to its origin when the active trigger ends.
    pub return_on_end: bool,
    pub behaviours: Vec<Behaviour>,
    /// Size of the internal cursor trail buffer. Set automatically by the builder
    /// from the largest `trail` value across all `FollowCursor` behaviours.
    pub trail_capacity: f32,
}

/// A single (trigger, action) pair on an [`Actor`].
pub struct Behaviour {
    pub trigger: Trigger,
    pub action: Action,
}

/// The condition that activates an actor [`Action`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Trigger {
    /// Always active. Used for persistent baseline behaviours (e.g. always follow cursor).
    Always,
    /// Active while the cursor is inside the actor's hitbox.
    OnHoverSelf,
    /// Active while the primary mouse button is held down over the actor's hitbox.
    OnPressSelf,
    /// Fires once on left-button release over the actor's hitbox. Latches until superseded.
    OnClickSelf,
    /// Fires once on any left-button release anywhere on screen. Latches until superseded.
    OnClickAnywhere,
}

/// What an actor does when its trigger is active.
#[derive(Debug, Clone)]
pub enum Action {
    /// Chase the cursor, optionally lagging behind by `trail` seconds.
    ///
    /// `speed` controls the lerp rate (higher = snappier). `trail: 0.0` means
    /// the actor aims directly at the current cursor position.
    FollowCursor { speed: f32, trail: f32 },

    /// Lerp toward a fixed point at `speed` units per second.
    ///
    /// `x` and `y` are in the centred coordinate space, added to the actor's origin.
    /// If `return_on_end` is set on the actor, it lerps back to origin when the trigger ends.
    MoveTo { x: f32, y: f32, speed: f32 },

    /// Temporarily display a different gif while the trigger is active.
    ///
    /// Reverts to the actor's base gif (or style) automatically when the trigger ends.
    /// The replacement gif loops continuously while active.
    SwapGif {
        texture: crate::textures::TextureId,
        shader: crate::draw::ShaderId,
    },
}

/// A transient notification that fades out after `duration` seconds.
///
/// Added to the active root via `push_toast`. Ticked like any other item;
/// expired toasts are removed back-to-front after each tick pass.
pub struct Toast {
    pub id: String,
    pub x: f32,
    pub y: f32,
    pub width: f32,
    pub height: f32,
    pub message: String,
    pub duration: f32,
    /// Shape used to draw the toast background and text.
    pub shape: Option<StyleId>,
}

// ── Tab ───────────────────────────────────────────────────────────────────────

/// One page inside a [`Tab`] container.
pub struct TabPage {
    /// Label shown on the selector button for this page (informational; not rendered by Tab itself).
    pub label: String,
    /// Children laid out vertically in the scrollable content area.
    pub items: Vec<UiItem>,
}

/// A content-switcher container. Renders the active page's children in a scrollable,
/// clipped area. Page switching is driven externally by buttons with `SwitchTabPage` actions.
pub struct Tab {
    pub id: String,
    pub x: f32,
    pub y: f32,
    pub width: f32,
    pub height: f32,
    pub pad_left: f32,
    pub pad_right: f32,
    pub pad_top: f32,
    pub pad_bottom: f32,
    pub gap: f32,
    pub style: Option<StyleId>,
    pub pages: Vec<TabPage>,
    pub full_span: bool,
}

// ── UiItem ────────────────────────────────────────────────────────────────────

/// Any item that can appear in a UI root or container.
///
/// `Spacer` is only meaningful inside a `Bar` — it expands to fill remaining main-axis space.
pub enum UiItem {
    Slider(Slider),
    Button(Button),
    ScrollList(ScrollList),
    Bar(Bar),
    Popout(Popout),
    Toggle(Toggle),
    TextBox(TextBox),
    Label(FreeLabel),
    Divider(Divider),
    Image(Image),
    ProgressBar(ProgressBar),
    ScrollPane(ScrollPane),
    Dropdown(Dropdown),
    RadioGroup(RadioGroup),
    Actor(Actor),
    Toast(Toast),
    Tab(Tab),
    /// Fills remaining space along a Bar's main axis. No-op outside of Bar.
    Spacer,
}

impl UiItem {
    /// The id string of this item, or `""` for items that have none (Spacer, Divider, Label).
    pub fn id(&self) -> &str {
        match self {
            Self::Slider(x) => &x.id,
            Self::Button(x) => &x.id,
            Self::ScrollList(x) => &x.id,
            Self::Bar(x) => &x.id,
            Self::Popout(x) => &x.id,
            Self::Toggle(x) => &x.id,
            Self::TextBox(x) => &x.id,
            Self::ProgressBar(x) => &x.id,
            Self::ScrollPane(x) => &x.id,
            Self::Dropdown(x) => &x.id,
            Self::RadioGroup(x) => &x.id,
            Self::Actor(x) => &x.id,
            Self::Toast(x) => &x.id,
            Self::Tab(x) => &x.id,
            Self::Label(_) | Self::Divider(_) | Self::Image(_) | Self::Spacer => "",
        }
    }
}

// ── Slider ────────────────────────────────────────────────────────────────────

/// A horizontal drag slider with separate track and thumb styles.
///
/// The thumb is square (`height × height`) and slides along the track.
/// Value fires on every drag movement via the slider's action.
pub struct Slider {
    pub id: String,
    pub x: f32,
    pub y: f32,
    pub width: f32,
    pub height: f32,
    pub min: f32,
    pub max: f32,
    /// Initial slider value used when the slider is first constructed.
    /// The live state lives on `SliderState::value`.
    pub default_value: f32,
    /// Optional snap increment. `None` = continuous.
    pub step: Option<f32>,
    pub tooltip: Option<String>,
    /// Shape for the full-width background track.
    pub style_track: StyleId,
    /// Shape for the draggable thumb.
    pub style_thumb: StyleId,
    pub action: SliderAction,
}

// ── TextBox ───────────────────────────────────────────────────────────────────

/// A text input field. Supports both single-line and multi-line modes.
///
/// `on_change` fires on every keystroke with the full current string.
/// `on_submit` fires when the user presses Enter (or Ctrl+Enter in multiline mode).
/// The field uses `style_idle` when unfocused and `style_focus` when focused.
pub struct TextBox {
    pub id: String,
    pub x: f32,
    pub y: f32,
    pub width: f32,
    pub height: f32,
    /// Initial text content used when the textbox is first constructed.
    /// The live state lives on `TextBoxState::text`.
    pub default_text: String,
    /// Ghost text shown when the field is empty and unfocused.
    pub placeholder: String,
    /// Maximum character count. `None` = unlimited.
    pub max_len: Option<usize>,
    pub tooltip: Option<String>,
    pub style_idle: StyleId,
    pub style_focus: StyleId,
    pub on_change: TextBoxAction,
    pub on_submit: TextBoxAction,
    /// When `true`, the displayed text is replaced with bullet characters (•).
    pub password: bool,
    /// When `true`, Enter inserts a newline instead of submitting; Ctrl+Enter submits.
    pub multiline: bool,
    /// Override the font size used for text rendering and caret measurement.
    /// `None` = derive from widget height automatically.
    pub font_size: Option<f32>,
}

// ── ScrollPane ────────────────────────────────────────────────────────────────

/// A scrollable container that can hold any mix of [`UiItem`]s.
///
/// Children are laid out vertically (or horizontally when `horizontal`). The visible
/// area is clipped to the pane's bounds. Scroll input (wheel, touch drag) is
/// handled internally.
pub struct ScrollPane {
    pub id: String,
    pub x: f32,
    pub y: f32,
    pub width: f32,
    pub height: f32,
    pub pad_left: f32,
    pub pad_right: f32,
    pub pad_top: f32,
    pub pad_bottom: f32,
    /// Gap between children.
    pub gap: f32,
    /// Optional background shape.
    pub style: Option<StyleId>,
    /// If `true`, children are laid out left-to-right; height is imposed, width from each child.
    /// If `false` (default), children are laid out top-to-bottom; width is imposed, height from each child.
    pub horizontal: bool,
    /// If `true`, spans the full screen along the scroll axis and centers on that axis.
    pub full_span: bool,
    /// If `true`, skip auto-layout. Items are positioned relative to the pane's origin
    /// using their RON x/y values; only scroll offset is applied each frame.
    pub manual: bool,
    pub items: Vec<UiItem>,
}

// ── Dropdown ──────────────────────────────────────────────────────────────────

/// A dropdown header that owns its option buttons directly.
/// When open, option buttons are ticked with offset below the header.
/// Selection is handled internally — no backchannel needed.
pub struct Dropdown {
    pub id: String,
    pub x: f32,
    pub y: f32,
    pub width: f32,
    pub height: f32,
    /// Initial selected option index. The live state lives on `DropdownState::selected`.
    pub default_selected: usize,
    /// Labels for each option — kept so the header can display the selected text.
    pub options: Vec<String>,
    /// Shape for the collapsed header button.
    pub style: StyleId,
    /// Optional background box drawn behind the open option list.
    pub style_list: Option<StyleId>,
    pub action: DropdownAction,
    /// Option buttons owned by this dropdown, ticked internally when open.
    pub items: Vec<Button>,
}

// ── RadioGroup ────────────────────────────────────────────────────────────────

/// A group of mutually exclusive option buttons, owned and ticked internally.
/// Each frame the container programs each button's style based on selection.
pub struct RadioGroup {
    pub id: String,
    /// Initial selected option index. The live state lives on `RadioGroupState::selected`.
    pub default_selected: usize,
    /// Labels — kept so dispatch can report the selected label in `UiMsg::Radio`.
    pub options: Vec<String>,
    /// Shape for unselected option buttons.
    pub style_idle: StyleId,
    /// Shape for the selected option button.
    pub style_selected: StyleId,
    pub action: RadioAction,
    /// Option buttons owned by this group, ticked internally each frame.
    pub items: Vec<Button>,
    /// X position of the group.
    pub x: f32,
    /// Y position of the group.
    pub y: f32,
}