Skip to main content

nightshade_api/
ui.rs

1//! Retained UI: panels anchored to the window that stack their children,
2//! themed buttons, and labels, with click and hover queries. These build on the
3//! same retained UI tree as [`spawn_text`](crate::prelude::spawn_text).
4
5use crate::text::ScreenAnchor;
6use crate::text::ui_root;
7use nightshade::ecs::ui::layout_types::FlowDirection;
8use nightshade::ecs::ui::units::UiValue;
9use nightshade::prelude::*;
10
11/// Turns on immediate-mode egui (requires the `egui` feature). Call it from
12/// setup, then draw each frame by pulling the frame's context with
13/// [`egui_context`](crate::prelude::egui_context) from your update closure or
14/// any system. `egui` and `egui_context` are re-exported from the prelude.
15///
16/// ```ignore
17/// run(
18///     |world| { enable_egui(world); spawn_cube(world, vec3(0.0, 0.5, 0.0)) },
19///     |world, cube| {
20///         if let Some(ctx) = egui_context(world) {
21///             egui::Window::new("Inspector").show(&ctx, |ui| ui.label(format!("{cube:?}")));
22///         }
23///     },
24/// )
25/// .unwrap();
26/// ```
27#[cfg(feature = "egui")]
28pub fn enable_egui(world: &mut World) {
29    world.plugin_resource_mut::<EguiState>().enabled = true;
30}
31
32/// Spawns an empty panel anchored to a window corner or the center, sized in
33/// pixels. It has a themed translucent background and stacks whatever you add to
34/// it from top to bottom. Fill it with [`panel_button`] and [`panel_label`].
35pub fn spawn_panel(world: &mut World, anchor: ScreenAnchor, width: f32, height: f32) -> Entity {
36    let root = ui_root(world);
37    let (position, anchor_kind) = panel_anchor(anchor);
38    let panel = {
39        let mut tree = UiTreeBuilder::from_parent(world, root);
40        tree.add_node()
41            .window(position, Ab(vec2(width, height)), anchor_kind)
42            .with_rect(8.0, 1.0, Vec4::new(1.0, 1.0, 1.0, 0.1))
43            .color_raw::<UiBase>(Vec4::new(0.05, 0.05, 0.08, 0.85))
44            .flow(FlowDirection::Vertical, 12.0, 8.0)
45            .entity()
46    };
47    ui_mark_render_dirty(world);
48    panel
49}
50
51/// Adds a themed button to a panel and returns it. Poll it every frame with
52/// [`button_clicked`].
53pub fn panel_button(world: &mut World, panel: Entity, text: &str) -> Entity {
54    let button = {
55        let mut tree = UiTreeBuilder::from_parent(world, panel);
56        tree.add_button(text)
57    };
58    ui_mark_render_dirty(world);
59    button
60}
61
62/// Adds a line of text to a panel and returns it. Update it with
63/// [`set_text`](crate::prelude::set_text) and restyle it with
64/// [`set_text_color`](crate::prelude::set_text_color) and
65/// [`set_text_size`](crate::prelude::set_text_size).
66pub fn panel_label(world: &mut World, panel: Entity, text: &str) -> Entity {
67    let label = {
68        let mut tree = UiTreeBuilder::from_parent(world, panel);
69        tree.add_node()
70            .flow_child(Rl(vec2(100.0, 0.0)) + Ab(vec2(0.0, 24.0)))
71            .with_text(text, 18.0)
72            .color_raw::<UiBase>(Vec4::new(1.0, 1.0, 1.0, 1.0))
73            .entity()
74    };
75    ui_mark_render_dirty(world);
76    label
77}
78
79/// Adds a labeled checkbox to a panel and returns it. Read it with
80/// [`checkbox_value`].
81pub fn panel_checkbox(world: &mut World, panel: Entity, label: &str, initial: bool) -> Entity {
82    let entity = {
83        let mut tree = UiTreeBuilder::from_parent(world, panel);
84        tree.add_checkbox(label, initial)
85    };
86    ui_mark_render_dirty(world);
87    entity
88}
89
90/// The checkbox's current on or off value.
91#[inline]
92pub fn checkbox_value(world: &World, checkbox: Entity) -> bool {
93    ui_checkbox_value(world, checkbox).unwrap_or(false)
94}
95
96/// Adds a slider from `min` to `max` starting at `initial` to a panel. Read it
97/// with [`slider_value`], set it with [`set_slider_value`].
98pub fn panel_slider(world: &mut World, panel: Entity, min: f32, max: f32, initial: f32) -> Entity {
99    let entity = {
100        let mut tree = UiTreeBuilder::from_parent(world, panel);
101        tree.add_slider(min, max, initial)
102    };
103    ui_mark_render_dirty(world);
104    entity
105}
106
107/// The slider's current value.
108#[inline]
109pub fn slider_value(world: &World, slider: Entity) -> f32 {
110    ui_slider_value(world, slider).unwrap_or(0.0)
111}
112
113/// Sets the slider's value.
114pub fn set_slider_value(world: &mut World, slider: Entity, value: f32) {
115    ui_slider_set_value(world, slider, value);
116    ui_mark_render_dirty(world);
117}
118
119/// Adds a single line text input with grey `placeholder` text to a panel. Read
120/// edits with [`text_input_changed`].
121pub fn panel_text_input(world: &mut World, panel: Entity, placeholder: &str) -> Entity {
122    let entity = {
123        let mut tree = UiTreeBuilder::from_parent(world, panel);
124        tree.add_text_input(placeholder)
125    };
126    ui_mark_render_dirty(world);
127    entity
128}
129
130/// The input's new contents if it changed this frame, else `None`.
131#[inline]
132pub fn text_input_changed(world: &World, input: Entity) -> Option<String> {
133    ui_text_input_changed(world, input).map(str::to_string)
134}
135
136/// Adds a dropdown of `options` with `initial` selected to a panel. Read it with
137/// [`dropdown_selected`].
138pub fn panel_dropdown(
139    world: &mut World,
140    panel: Entity,
141    options: &[&str],
142    initial: usize,
143) -> Entity {
144    let entity = {
145        let mut tree = UiTreeBuilder::from_parent(world, panel);
146        tree.add_dropdown(options, initial)
147    };
148    ui_mark_render_dirty(world);
149    entity
150}
151
152/// The newly chosen index if the dropdown selection changed this frame.
153#[inline]
154pub fn dropdown_selected(world: &World, dropdown: Entity) -> Option<usize> {
155    ui_dropdown_selected_changed(world, dropdown)
156}
157
158/// Adds a progress bar filled to `initial` (0.0 to 1.0) to a panel. Update it
159/// with [`set_progress`].
160pub fn panel_progress_bar(world: &mut World, panel: Entity, initial: f32) -> Entity {
161    let entity = {
162        let mut tree = UiTreeBuilder::from_parent(world, panel);
163        tree.add_progress_bar(initial)
164    };
165    ui_mark_render_dirty(world);
166    entity
167}
168
169/// Sets a progress bar's fill, 0.0 to 1.0.
170pub fn set_progress(world: &mut World, bar: Entity, value: f32) {
171    ui_progress_bar_set_value(world, bar, value);
172    ui_mark_render_dirty(world);
173}
174
175/// Adds an on or off toggle starting at `initial` to a panel. Read it with
176/// [`toggle_value`].
177pub fn panel_toggle(world: &mut World, panel: Entity, initial: bool) -> Entity {
178    let entity = {
179        let mut tree = UiTreeBuilder::from_parent(world, panel);
180        tree.add_toggle(initial)
181    };
182    ui_mark_render_dirty(world);
183    entity
184}
185
186/// The toggle's current on or off value.
187#[inline]
188pub fn toggle_value(world: &World, toggle: Entity) -> bool {
189    ui_toggle_value(world, toggle).unwrap_or(false)
190}
191
192/// Adds a radio button to a panel, one option of a group identified by
193/// `group_id`. Radios sharing a group are mutually exclusive. Read the group's
194/// choice with [`radio_selected`].
195pub fn panel_radio(
196    world: &mut World,
197    panel: Entity,
198    label: &str,
199    group_id: u32,
200    option_index: usize,
201) -> Entity {
202    let entity = {
203        let mut tree = UiTreeBuilder::from_parent(world, panel);
204        tree.add_radio(label, group_id, option_index)
205    };
206    ui_mark_render_dirty(world);
207    entity
208}
209
210/// The selected option index of a radio group, if any option is selected.
211#[inline]
212pub fn radio_selected(world: &World, group_id: u32) -> Option<usize> {
213    ui_radio_group_value(world, group_id)
214}
215
216/// Adds a dual-handle range slider to a panel, selecting a span from `low` to
217/// `high` within `min` and `max`. Set both handles with [`set_range`].
218pub fn panel_range_slider(
219    world: &mut World,
220    panel: Entity,
221    min: f32,
222    max: f32,
223    low: f32,
224    high: f32,
225) -> Entity {
226    let entity = {
227        let mut tree = UiTreeBuilder::from_parent(world, panel);
228        tree.add_range_slider(min, max, low, high)
229    };
230    ui_mark_render_dirty(world);
231    entity
232}
233
234/// Sets both handles of a range slider.
235pub fn set_range(world: &mut World, slider: Entity, low: f32, high: f32) {
236    ui_range_slider_set_values(world, slider, low, high);
237    ui_mark_render_dirty(world);
238}
239
240/// Adds a tab bar of `labels` to a panel with `initial` selected. Switch the
241/// active tab with [`set_tab`].
242pub fn panel_tabs(world: &mut World, panel: Entity, labels: &[&str], initial: usize) -> Entity {
243    let entity = {
244        let mut tree = UiTreeBuilder::from_parent(world, panel);
245        tree.add_tab_bar(labels, initial)
246    };
247    ui_mark_render_dirty(world);
248    entity
249}
250
251/// Selects a tab bar's active tab by index.
252pub fn set_tab(world: &mut World, tabs: Entity, index: usize) {
253    ui_tab_bar_set_value(world, tabs, index);
254    ui_mark_render_dirty(world);
255}
256
257/// Adds a collapsing section titled `label` to a panel and returns its content
258/// container. Add child widgets to the returned entity; they hide and show as
259/// the header is toggled.
260pub fn panel_collapsing(world: &mut World, panel: Entity, label: &str, open: bool) -> Entity {
261    let entity = {
262        let mut tree = UiTreeBuilder::from_parent(world, panel);
263        tree.add_collapsing_header(label, open)
264    };
265    ui_mark_render_dirty(world);
266    entity
267}
268
269/// Adds a color picker to a panel starting at `initial` linear RGBA. Read the
270/// chosen color with [`color_value`].
271pub fn panel_color_picker(world: &mut World, panel: Entity, initial: [f32; 4]) -> Entity {
272    let entity = {
273        let mut tree = UiTreeBuilder::from_parent(world, panel);
274        tree.add_color_picker(Vec4::new(initial[0], initial[1], initial[2], initial[3]))
275    };
276    ui_mark_render_dirty(world);
277    entity
278}
279
280/// The color picker's current color as linear RGBA.
281pub fn color_value(world: &World, picker: Entity) -> [f32; 4] {
282    ui_color_picker_color(world, picker)
283        .map(|color| [color.x, color.y, color.z, color.w])
284        .unwrap_or([1.0, 1.0, 1.0, 1.0])
285}
286
287/// Whether the button was clicked this frame, a press and release on the same
288/// button. Poll this every frame.
289#[inline]
290pub fn button_clicked(world: &World, button: Entity) -> bool {
291    ui_clicked(world, button)
292}
293
294/// Whether the pointer is currently over the button.
295#[inline]
296pub fn button_hovered(world: &World, button: Entity) -> bool {
297    world
298        .get::<nightshade::ecs::ui::components::UiNodeInteraction>(button)
299        .is_some_and(|interaction| interaction.hovered)
300}
301
302/// Removes a panel and everything in it.
303#[inline]
304pub fn despawn_panel(world: &mut World, panel: Entity) {
305    despawn_recursive_immediate(world, panel);
306    ui_mark_render_dirty(world);
307}
308
309/// Adds a horizontal row to a panel and returns it. Widgets added to the row
310/// lay out left to right instead of the panel's top to bottom stacking, which is
311/// how you put a label next to a value or a run of buttons on one line.
312pub fn panel_row(world: &mut World, panel: Entity, height: f32) -> Entity {
313    let row = {
314        let mut tree = UiTreeBuilder::from_parent(world, panel);
315        tree.add_node()
316            .flow_child(Rl(vec2(100.0, 0.0)) + Ab(vec2(0.0, height)))
317            .flow(FlowDirection::Horizontal, 0.0, 8.0)
318            .entity()
319    };
320    ui_mark_render_dirty(world);
321    row
322}
323
324/// Adds a fixed-column grid to a panel and returns it. Children flow into
325/// `columns` cells of `row_height`, wrapping to a new row as they fill, for
326/// inventory grids, palettes, and icon sheets.
327pub fn panel_grid(
328    world: &mut World,
329    panel: Entity,
330    columns: usize,
331    row_height: f32,
332    height: f32,
333) -> Entity {
334    let grid = {
335        let mut tree = UiTreeBuilder::from_parent(world, panel);
336        tree.add_node()
337            .flow_child(Rl(vec2(100.0, 0.0)) + Ab(vec2(0.0, height)))
338            .grid(columns, row_height, 0.0, 8.0, 8.0)
339            .entity()
340    };
341    ui_mark_render_dirty(world);
342    grid
343}
344
345/// Adds a scrollable region of the given pixel `height` to a panel and returns
346/// its content container. Add widgets to the returned entity; when they exceed
347/// the height the region scrolls with the mouse wheel and a draggable thumb.
348/// This is what dense inventory and dialogue lists need.
349pub fn panel_scroll(world: &mut World, panel: Entity, height: f32) -> Entity {
350    let content = {
351        let mut tree = UiTreeBuilder::from_parent(world, panel);
352        let area = tree.add_scroll_area(vec2(0.0, height));
353        widget::<UiScrollAreaData>(tree.world_mut(), area)
354            .map(|data| data.content_entity)
355            .unwrap_or(area)
356    };
357    ui_mark_render_dirty(world);
358    content
359}
360
361/// Scrolls a [`panel_scroll`] region (pass the panel-scroll entity, not the
362/// content) to a pixel offset from the top.
363pub fn set_scroll_offset(world: &mut World, scroll_area: Entity, offset: f32) {
364    ui_scroll_area_set_offset(world, scroll_area, offset);
365    ui_mark_render_dirty(world);
366}
367
368/// Sets a widget's keyboard focus order. Widgets with a focus order participate
369/// in Tab navigation, lowest order first. Use it to make a form's fields step
370/// in a sensible sequence.
371pub fn set_focus_order(world: &mut World, entity: Entity, order: i32) {
372    if let Some(interaction) =
373        world.get_mut::<nightshade::ecs::ui::components::UiNodeInteraction>(entity)
374    {
375        interaction.tab_index = Some(order);
376    }
377}
378
379/// Gives keyboard focus to a widget immediately, as if the user had tabbed to
380/// it. Useful to focus the first field when a form opens.
381pub fn focus_widget(world: &mut World, entity: Entity) {
382    world
383        .res_mut::<nightshade::ecs::ui::resources::RetainedUiState>()
384        .interaction
385        .focused_entity = Some(entity);
386}
387
388/// Adds a multi-line text area with grey `placeholder` text and `rows` visible
389/// rows to a panel. Set its contents with [`set_text_area`].
390pub fn panel_text_area(world: &mut World, panel: Entity, placeholder: &str, rows: usize) -> Entity {
391    let entity = {
392        let mut tree = UiTreeBuilder::from_parent(world, panel);
393        tree.add_text_area(placeholder, rows)
394    };
395    ui_mark_render_dirty(world);
396    entity
397}
398
399/// Adds a multi-line text area pre-filled with `initial` to a panel.
400pub fn panel_text_area_with_value(
401    world: &mut World,
402    panel: Entity,
403    placeholder: &str,
404    rows: usize,
405    initial: &str,
406) -> Entity {
407    let entity = {
408        let mut tree = UiTreeBuilder::from_parent(world, panel);
409        tree.add_text_area_with_value(placeholder, rows, initial)
410    };
411    ui_mark_render_dirty(world);
412    entity
413}
414
415/// Replaces a text area's contents.
416pub fn set_text_area(world: &mut World, area: Entity, text: &str) {
417    ui_text_area_set_value(world, area, text);
418    ui_mark_render_dirty(world);
419}
420
421/// Adds a multi-select chip list of `options` to a panel. Set which indices are
422/// chosen with [`set_multi_select`].
423pub fn panel_multi_select(world: &mut World, panel: Entity, options: &[&str]) -> Entity {
424    let entity = {
425        let mut tree = UiTreeBuilder::from_parent(world, panel);
426        tree.add_multi_select(options)
427    };
428    ui_mark_render_dirty(world);
429    entity
430}
431
432/// Sets a multi-select's chosen option indices.
433pub fn set_multi_select(world: &mut World, widget: Entity, indices: &[usize]) {
434    ui_multi_select_set_selected(world, widget, indices);
435    ui_mark_render_dirty(world);
436}
437
438/// Adds a date picker starting at the given date to a panel. Set it later with
439/// [`set_date`].
440pub fn panel_date_picker(
441    world: &mut World,
442    panel: Entity,
443    year: i32,
444    month: u32,
445    day: u32,
446) -> Entity {
447    let entity = {
448        let mut tree = UiTreeBuilder::from_parent(world, panel);
449        tree.add_date_picker(year, month, day)
450    };
451    ui_mark_render_dirty(world);
452    entity
453}
454
455/// Sets a date picker's value.
456pub fn set_date(world: &mut World, picker: Entity, year: i32, month: u32, day: u32) {
457    ui_date_picker_set_value(world, picker, year, month, day);
458    ui_mark_render_dirty(world);
459}
460
461/// Adds a dropdown menu labeled `label` listing `items` to a panel.
462pub fn panel_menu(world: &mut World, panel: Entity, label: &str, items: &[&str]) -> Entity {
463    let entity = {
464        let mut tree = UiTreeBuilder::from_parent(world, panel);
465        tree.add_menu(label, items)
466    };
467    ui_mark_render_dirty(world);
468    entity
469}
470
471/// Adds an HSV color picker starting at `initial` linear RGBA to a panel. Read it
472/// with [`color_value`].
473pub fn panel_color_picker_hsv(world: &mut World, panel: Entity, initial: [f32; 4]) -> Entity {
474    let entity = {
475        let mut tree = UiTreeBuilder::from_parent(world, panel);
476        tree.add_color_picker_hsv(rgba(initial))
477    };
478    ui_mark_render_dirty(world);
479    entity
480}
481
482/// Adds a draggable splitter dividing a panel into two resizable panes at
483/// `initial_ratio` (0.0 to 1.0). Returns the splitter; its two panes are its
484/// children. `direction` is [`SplitDirection::Horizontal`] or `Vertical`.
485pub fn panel_splitter(
486    world: &mut World,
487    panel: Entity,
488    direction: SplitDirection,
489    initial_ratio: f32,
490) -> Entity {
491    let entity = {
492        let mut tree = UiTreeBuilder::from_parent(world, panel);
493        tree.add_splitter(direction, initial_ratio)
494    };
495    ui_mark_render_dirty(world);
496    entity
497}
498
499/// Adds a breadcrumb trail of `segments` to a panel, for showing a path or
500/// navigation hierarchy.
501pub fn panel_breadcrumb(world: &mut World, panel: Entity, segments: &[&str]) -> Entity {
502    let entity = {
503        let mut tree = UiTreeBuilder::from_parent(world, panel);
504        tree.add_breadcrumb(segments)
505    };
506    ui_mark_render_dirty(world);
507    entity
508}
509
510/// Adds a virtual list to a panel that recycles a pool of `pool_size` rows of
511/// `item_height` pixels each, so it scrolls thousands of items cheaply.
512pub fn panel_virtual_list(
513    world: &mut World,
514    panel: Entity,
515    item_height: f32,
516    pool_size: usize,
517) -> Entity {
518    let entity = {
519        let mut tree = UiTreeBuilder::from_parent(world, panel);
520        tree.add_virtual_list(item_height, pool_size)
521    };
522    ui_mark_render_dirty(world);
523    entity
524}
525
526/// Adds a simple table with column `headers` of the given pixel `widths` to a
527/// panel. For large or sortable data use [`panel_data_grid`].
528pub fn panel_table(world: &mut World, panel: Entity, headers: &[&str], widths: &[f32]) -> Entity {
529    let entity = {
530        let mut tree = UiTreeBuilder::from_parent(world, panel);
531        tree.add_table(headers, widths)
532    };
533    ui_mark_render_dirty(world);
534    entity
535}
536
537/// Adds a sortable, selectable data grid to a panel. `columns` are `(header,
538/// width)` pairs and `pool_size` is how many rows to keep instantiated for
539/// recycling. Fill cells with [`set_data_grid_rows`] and [`set_data_grid_cell`],
540/// read clicks with [`data_grid_selection_changed`].
541pub fn panel_data_grid(
542    world: &mut World,
543    panel: Entity,
544    columns: &[(&str, f32)],
545    pool_size: usize,
546) -> Entity {
547    let grid_columns: Vec<DataGridColumn> = columns
548        .iter()
549        .map(|(header, width)| DataGridColumn::new(header, *width))
550        .collect();
551    let entity = {
552        let mut tree = UiTreeBuilder::from_parent(world, panel);
553        tree.add_data_grid(&grid_columns, pool_size)
554    };
555    ui_mark_render_dirty(world);
556    entity
557}
558
559/// Sets a data grid's total row count.
560pub fn set_data_grid_rows(world: &mut World, grid: Entity, count: usize) {
561    ui_data_grid_set_row_count(world, grid, count);
562}
563
564/// Sets the text of a single data grid cell.
565pub fn set_data_grid_cell(world: &mut World, grid: Entity, row: usize, column: usize, text: &str) {
566    ui_data_grid_set_cell(world, grid, row, column, text);
567}
568
569/// Whether the data grid's row selection changed this frame.
570#[inline]
571pub fn data_grid_selection_changed(world: &World, grid: Entity) -> bool {
572    ui_data_grid_selection_changed(world, grid)
573}
574
575/// Adds a command palette to a panel: a searchable overlay of actions opened
576/// from a hotkey, keeping `pool_size` result rows instantiated.
577pub fn panel_command_palette(world: &mut World, panel: Entity, pool_size: usize) -> Entity {
578    let entity = {
579        let mut tree = UiTreeBuilder::from_parent(world, panel);
580        tree.add_command_palette(pool_size)
581    };
582    ui_mark_render_dirty(world);
583    entity
584}
585
586/// Adds a property grid to a panel, a two-column label and value layout for
587/// inspectors. `label_width` is the pixel width of the label column. Add rows
588/// with [`panel_property_row`].
589pub fn panel_property_grid(world: &mut World, panel: Entity, label_width: f32) -> Entity {
590    let entity = {
591        let mut tree = UiTreeBuilder::from_parent(world, panel);
592        tree.add_property_grid(label_width)
593    };
594    ui_mark_render_dirty(world);
595    entity
596}
597
598/// Adds a labeled row to a [`panel_property_grid`] and returns its value cell.
599/// Add the value widget (a slider, text input, and so on) to the returned cell.
600pub fn panel_property_row(world: &mut World, grid: Entity, label: &str) -> Entity {
601    let row = {
602        let mut tree = UiTreeBuilder::from_parent(world, grid);
603        tree.add_property_row(grid, grid, label)
604    };
605    ui_mark_render_dirty(world);
606    row
607}
608
609/// Adds a tree view to a panel. Add root nodes to its [`tree_content`]
610/// container with [`tree_node`], read the selection with [`tree_view_selected`].
611pub fn panel_tree_view(world: &mut World, panel: Entity, multi_select: bool) -> Entity {
612    let entity = {
613        let mut tree = UiTreeBuilder::from_parent(world, panel);
614        tree.add_tree_view(multi_select)
615    };
616    ui_mark_render_dirty(world);
617    entity
618}
619
620/// The root container of a [`panel_tree_view`]; add top-level [`tree_node`]s to
621/// it.
622pub fn tree_content(world: &World, tree_view: Entity) -> Entity {
623    widget::<UiTreeViewData>(world, tree_view)
624        .map(|data| data.content_entity)
625        .unwrap_or(tree_view)
626}
627
628/// Adds a node labeled `label` at `depth` under `parent_container` (a tree's
629/// [`tree_content`] for a root node, or another node's [`tree_node_children`]
630/// for a nested one). `user_data` is an id you choose to recognize the node.
631pub fn tree_node(
632    world: &mut World,
633    tree_view: Entity,
634    parent_container: Entity,
635    label: &str,
636    depth: usize,
637    user_data: u64,
638) -> Entity {
639    let node = {
640        let mut tree = UiTreeBuilder::from_parent(world, parent_container);
641        tree.add_tree_node(tree_view, parent_container, label, depth, user_data)
642    };
643    ui_mark_render_dirty(world);
644    node
645}
646
647/// The container a node's children go into; pass it as `parent_container` to
648/// [`tree_node`] to nest under this node.
649pub fn tree_node_children(world: &World, node: Entity) -> Entity {
650    widget::<UiTreeNodeData>(world, node)
651        .map(|data| data.children_container)
652        .unwrap_or(node)
653}
654
655/// Expands or collapses a [`tree_node`].
656pub fn set_tree_node_expanded(world: &mut World, node: Entity, expanded: bool) {
657    ui_tree_node_set_expanded(world, node, expanded);
658    ui_mark_render_dirty(world);
659}
660
661/// The entities of the currently selected [`tree_node`]s.
662#[inline]
663pub fn tree_view_selected(world: &World, tree_view: Entity) -> Vec<Entity> {
664    ui_tree_view_selected(world, tree_view)
665}
666
667/// Adds a numeric drag value from `min` to `max` starting at `initial` to a
668/// panel: a field whose value scrubs as you drag across it. Read it with
669/// [`drag_value`].
670pub fn panel_drag_value(
671    world: &mut World,
672    panel: Entity,
673    min: f32,
674    max: f32,
675    initial: f32,
676) -> Entity {
677    let entity = {
678        let mut tree = UiTreeBuilder::from_parent(world, panel);
679        tree.add_drag_value(min, max, initial)
680    };
681    ui_mark_render_dirty(world);
682    entity
683}
684
685/// The drag value's current value.
686#[inline]
687pub fn drag_value(world: &World, widget: Entity) -> f32 {
688    ui_drag_value(world, widget).unwrap_or(0.0)
689}
690
691/// Adds a selectable label to a panel. Labels sharing a `group` are mutually
692/// exclusive, like a list selection. Pass `None` for a standalone toggle.
693pub fn panel_selectable(
694    world: &mut World,
695    panel: Entity,
696    text: &str,
697    group: Option<u32>,
698) -> Entity {
699    let entity = {
700        let mut tree = UiTreeBuilder::from_parent(world, panel);
701        tree.add_selectable_label(text, group)
702    };
703    ui_mark_render_dirty(world);
704    entity
705}
706
707/// Adds a centered modal dialog titled `title`, `width` by `height` pixels, to a
708/// panel and returns its content container. Add the dialog's body to the
709/// returned entity; toggle the whole modal with [`set_panel_visible`].
710pub fn panel_modal(
711    world: &mut World,
712    panel: Entity,
713    title: &str,
714    width: f32,
715    height: f32,
716) -> Entity {
717    let entity = {
718        let mut tree = UiTreeBuilder::from_parent(world, panel);
719        tree.add_modal_dialog(title, width, height)
720    };
721    ui_mark_render_dirty(world);
722    entity
723}
724
725/// Adds an animated loading spinner to a panel.
726pub fn panel_spinner(world: &mut World, panel: Entity) -> Entity {
727    let entity = {
728        let mut tree = UiTreeBuilder::from_parent(world, panel);
729        tree.add_spinner()
730    };
731    ui_mark_render_dirty(world);
732    entity
733}
734
735/// Adds a thin horizontal divider line to a panel.
736pub fn panel_separator(world: &mut World, panel: Entity) -> Entity {
737    let entity = {
738        let mut tree = UiTreeBuilder::from_parent(world, panel);
739        tree.add_separator()
740    };
741    ui_mark_render_dirty(world);
742    entity
743}
744
745/// Adds a larger heading-styled line of text to a panel.
746pub fn panel_heading(world: &mut World, panel: Entity, text: &str) -> Entity {
747    let entity = {
748        let mut tree = UiTreeBuilder::from_parent(world, panel);
749        tree.add_heading(text)
750    };
751    ui_mark_render_dirty(world);
752    entity
753}
754
755fn panel_anchor(anchor: ScreenAnchor) -> (UiValue<Vec2>, Anchor) {
756    match anchor {
757        ScreenAnchor::TopLeft => (Ab(vec2(20.0, 20.0)).into(), Anchor::TopLeft),
758        ScreenAnchor::TopCenter => (Rl(vec2(50.0, 0.0)) + Ab(vec2(0.0, 20.0)), Anchor::TopCenter),
759        ScreenAnchor::TopRight => (
760            Rl(vec2(100.0, 0.0)) + Ab(vec2(-20.0, 20.0)),
761            Anchor::TopRight,
762        ),
763        ScreenAnchor::BottomLeft => (
764            Rl(vec2(0.0, 100.0)) + Ab(vec2(20.0, -20.0)),
765            Anchor::BottomLeft,
766        ),
767        ScreenAnchor::BottomCenter => (
768            Rl(vec2(50.0, 100.0)) + Ab(vec2(0.0, -20.0)),
769            Anchor::BottomCenter,
770        ),
771        ScreenAnchor::BottomRight => (
772            Rl(vec2(100.0, 100.0)) + Ab(vec2(-20.0, -20.0)),
773            Anchor::BottomRight,
774        ),
775        ScreenAnchor::Center => (Rl(vec2(50.0, 50.0)).into(), Anchor::Center),
776    }
777}
778
779/// The anchor's relative base position (as a percentage for [`Rl`]) and engine
780/// anchor, with no margin, so a caller can add its own pixel offset.
781fn anchor_base(anchor: ScreenAnchor) -> (Vec2, Anchor) {
782    match anchor {
783        ScreenAnchor::TopLeft => (vec2(0.0, 0.0), Anchor::TopLeft),
784        ScreenAnchor::TopCenter => (vec2(50.0, 0.0), Anchor::TopCenter),
785        ScreenAnchor::TopRight => (vec2(100.0, 0.0), Anchor::TopRight),
786        ScreenAnchor::BottomLeft => (vec2(0.0, 100.0), Anchor::BottomLeft),
787        ScreenAnchor::BottomCenter => (vec2(50.0, 100.0), Anchor::BottomCenter),
788        ScreenAnchor::BottomRight => (vec2(100.0, 100.0), Anchor::BottomRight),
789        ScreenAnchor::Center => (vec2(50.0, 50.0), Anchor::Center),
790    }
791}
792
793fn rgba(color: [f32; 4]) -> Vec4 {
794    Vec4::new(color[0], color[1], color[2], color[3])
795}
796
797const PANEL_BORDER: Vec4 = Vec4::new(0.12, 0.14, 0.2, 0.7);
798const PANEL_SHADOW: Vec4 = Vec4::new(0.0, 0.0, 0.0, 0.55);
799
800/// Spawns a panel anchored to any of the nine screen positions at a pixel
801/// `offset` from that anchor, `size` pixels large, filled with `color`. Unlike
802/// [`spawn_panel`] this gives full control over placement, size, and color for a
803/// custom HUD; fill it with absolutely positioned [`panel_text`], [`panel_box`],
804/// and [`panel_button_at`] children. Returns the panel entity.
805pub fn spawn_panel_at(
806    world: &mut World,
807    anchor: ScreenAnchor,
808    offset: Vec2,
809    size: Vec2,
810    color: [f32; 4],
811) -> Entity {
812    let root = ui_root(world);
813    let (base, anchor_kind) = anchor_base(anchor);
814    let panel = {
815        let mut tree = UiTreeBuilder::from_parent(world, root);
816        tree.add_node()
817            .window(Rl(base) + Ab(offset), Ab(size), anchor_kind)
818            .with_rect(10.0, 1.5, PANEL_BORDER)
819            .color_raw::<UiBase>(rgba(color))
820            .with_shadow(PANEL_SHADOW, vec2(0.0, 6.0), 18.0, 0.0)
821            .entity()
822    };
823    ui_mark_render_dirty(world);
824    panel
825}
826
827/// Adds a text label to `parent` in the pixel rectangle `rect` (`[x, y, width,
828/// height]` from the parent's top left), at `font_size` in `color` with the
829/// given horizontal alignment. Returns the label entity; update it with
830/// [`set_panel_text`] and recolor it with [`set_panel_text_color`].
831pub fn panel_text(
832    world: &mut World,
833    parent: Entity,
834    text: &str,
835    rect: [f32; 4],
836    font_size: f32,
837    color: [f32; 4],
838    align: TextAlignment,
839) -> Entity {
840    let label = {
841        let mut tree = UiTreeBuilder::from_parent(world, parent);
842        tree.add_node()
843            .window(
844                Ab(vec2(rect[0], rect[1])),
845                Ab(vec2(rect[2], rect[3])),
846                Anchor::TopLeft,
847            )
848            .with_text(text, font_size)
849            .with_text_alignment(align, VerticalAlignment::Middle)
850            .color_raw::<UiBase>(rgba(color))
851            .without_pointer_events()
852            .entity()
853    };
854    ui_mark_render_dirty(world);
855    label
856}
857
858/// Adds a solid colored rectangle to `parent` at a pixel `offset`, sized `size`.
859/// Use it for bars, swatches, and dividers. Resize it with [`set_panel_rect`]
860/// and recolor it with [`set_panel_color`].
861pub fn panel_box(
862    world: &mut World,
863    parent: Entity,
864    offset: Vec2,
865    size: Vec2,
866    color: [f32; 4],
867) -> Entity {
868    let box_entity = {
869        let mut tree = UiTreeBuilder::from_parent(world, parent);
870        tree.add_node()
871            .window(Ab(offset), Ab(size), Anchor::TopLeft)
872            .with_rect(6.0, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
873            .color_raw::<UiBase>(rgba(color))
874            .without_pointer_events()
875            .entity()
876    };
877    ui_mark_render_dirty(world);
878    box_entity
879}
880
881/// Adds an interactive button to `parent` at a pixel `offset`, sized `size`,
882/// with a centered `label` (pass `""` for none and add your own children). It
883/// has themed hover and press states and a selected state for
884/// [`set_panel_selected`]. Poll it with [`button_clicked`]. Returns the button.
885pub fn panel_button_at(
886    world: &mut World,
887    parent: Entity,
888    label: &str,
889    offset: Vec2,
890    size: Vec2,
891) -> Entity {
892    let button = {
893        let mut tree = UiTreeBuilder::from_parent(world, parent);
894        tree.add_node()
895            .window(Ab(offset), Ab(size), Anchor::TopLeft)
896            .with_rect(8.0, 1.5, PANEL_BORDER)
897            .color_raw::<UiBase>(Vec4::new(0.05, 0.06, 0.09, 0.92))
898            .color_raw::<UiHover>(Vec4::new(0.09, 0.11, 0.17, 0.95))
899            .color_raw::<UiPressed>(Vec4::new(0.04, 0.05, 0.08, 1.0))
900            .with_transition::<UiHover>(12.0, 6.0)
901            .with_transition::<UiPressed>(18.0, 10.0)
902            .with_transition::<UiSelected>(10.0, 5.0)
903            .with_interaction()
904            .entity()
905    };
906    if !label.is_empty() {
907        let mut tree = UiTreeBuilder::from_parent(world, button);
908        tree.add_node()
909            .window(Rl(vec2(50.0, 50.0)), Ab(size), Anchor::Center)
910            .with_text(label, 14.0)
911            .with_text_alignment(TextAlignment::Center, VerticalAlignment::Middle)
912            .color_raw::<UiBase>(Vec4::new(0.92, 0.94, 1.0, 1.0))
913            .without_pointer_events()
914            .entity();
915    }
916    ui_mark_render_dirty(world);
917    button
918}
919
920/// Repositions and resizes a UI node within its parent, in pixels. Drives a bar
921/// fill or any element that changes size at runtime.
922pub fn set_panel_rect(world: &mut World, node: Entity, offset: Vec2, size: Vec2) {
923    if let Some(layout) = world.get_mut::<nightshade::ecs::ui::components::UiLayoutNode>(node) {
924        layout.base_layout = Some(UiLayoutType::Window(WindowLayout {
925            position: Ab(offset).into(),
926            size: Ab(size).into(),
927            anchor: Anchor::TopLeft,
928        }));
929    }
930    ui_mark_render_dirty(world);
931}
932
933/// Sets a UI node's background color (linear RGBA), for panels, boxes, and bars.
934pub fn set_panel_color(world: &mut World, node: Entity, color: [f32; 4]) {
935    if let Some(node_color) = world.get_mut::<nightshade::ecs::ui::components::UiNodeColor>(node) {
936        node_color.colors[UiBase::INDEX] = Some(rgba(color));
937    }
938    ui_mark_render_dirty(world);
939}
940
941/// Replaces a [`panel_text`] label's content. Skips work when unchanged, so it
942/// is fine to call every frame with a formatted string.
943pub fn set_panel_text(world: &mut World, label: Entity, text: &str) {
944    ui_set_text(world, label, text);
945}
946
947/// Recolors a [`panel_text`] label.
948pub fn set_panel_text_color(world: &mut World, label: Entity, color: [f32; 4]) {
949    set_panel_color(world, label, color);
950}
951
952/// Toggles a button's selected highlight, tinting it with `accent` while
953/// selected. Use it to show the active choice in a row of options.
954pub fn set_panel_selected(world: &mut World, button: Entity, selected: bool, accent: [f32; 4]) {
955    if let Some(node_color) = world.get_mut::<nightshade::ecs::ui::components::UiNodeColor>(button)
956    {
957        node_color.colors[UiSelected::INDEX] = Some(rgba(accent));
958    }
959    ui_set_selected(world, button, selected);
960    ui_mark_render_dirty(world);
961}
962
963/// Shows or hides a UI node and its children, for toggling overlays like a wave
964/// banner or a game over panel.
965pub fn set_panel_visible(world: &mut World, node: Entity, visible: bool) {
966    ui_set_visible(world, node, visible);
967    ui_mark_render_dirty(world);
968}