Skip to main content

yog_ui/
lib.rs

1//! yog-ui — retained-mode UI framework for Yog mods.
2//!
3//! Flexbox-inspired layout engine and a WPF-style [`grid`](widget::grid)
4//! (rows/columns: pixels, `Auto`, `*`) + GPU rendering via [`yog-gfx`].
5//! Use for custom inventories, guide books, tooltips, HUD overlays;
6//! [`surface`]s draw whatever the widgets don't (any `yog-gfx` call).
7//!
8//! # Quick start
9//! ```ignore
10//! use yog_ui::{UiRoot, widget, Align, FlexDir, Units};
11//!
12//! let ui = UiRoot::new("mymod:main_menu")
13//!     .style(|s| s.bg(0x88332211).padding(8.0, 8.0, 8.0, 8.0))
14//!     .child(
15//!         widget::panel(FlexDir::Column).gap(4.0)
16//!             .child(widget::label("Hello, World!").color(0xFF_DDAA00))
17//!             .child(widget::button("Click me").on_click("mymod:btn_click"))
18//!     );
19//! ```
20
21mod drawing;
22mod elements;
23mod interaction;
24mod typography;
25
26pub use drawing::layout;
27pub use elements::{attr, component, image, inventory_ids, surface, theme, widget};
28pub use interaction::{event, scroll, textbox};
29pub use typography::{rich, text};
30
31pub use drawing::render::{
32    ICON_SIZE, INV_SLOT_FACE, INV_SLOT_LIGHT, INV_SLOT_SHADOW, inv_slot_bevel,
33};
34pub use event::UiEvent;
35pub use layout::{Align, FlexDir, GridLines, LayoutNode, Rect, hit_path, node_at, set_focus};
36pub use scroll::ScrollState;
37pub use surface::{Pointer, Surface};
38pub use textbox::{TextBoxEvent, TextBoxState};
39pub use widget::{
40    Action, Cell, Dock, FocusStyle, GridLength, GridSpec, HAlign, StateFlags, StateStyles, Stretch,
41    StylePatch, Track, Units, VAlign, Widget,
42};
43pub use yog_gfx::{Color, Vec2};
44
45use yog_gfx::GfxContext;
46
47/// Top-level UI tree.  Build it, call [`layout`], then [`render`] each frame.
48pub struct UiRoot {
49    pub id: String,
50    pub root: Widget,
51    pub layout_root: LayoutNode,
52    pub needs_layout: bool,
53}
54
55impl UiRoot {
56    pub fn new(id: impl Into<String>, root: Widget) -> Self {
57        Self {
58            id: id.into(),
59            root,
60            layout_root: LayoutNode::default(),
61            needs_layout: true,
62        }
63    }
64
65    /// Recalculate layout. Call after changing the tree or on window resize.
66    pub fn layout(&mut self, screen_w: f32, screen_h: f32) {
67        self.layout_root = layout::compute(&self.root, screen_w, screen_h);
68        self.needs_layout = false;
69    }
70
71    /// Render the UI tree via `yog-gfx` draw2d.
72    /// Must be called from `on_hud_render`.
73    pub fn render(&self, ctx: &GfxContext) {
74        text::use_host_metrics(ctx);
75        image::use_host_images(ctx);
76        let d2d = ctx.draw2d();
77        drawing::render::render_node(ctx, &d2d, &self.root, &self.layout_root);
78    }
79
80    /// The tooltip of the innermost widget at `(mx, my)` that has one.
81    /// Call after [`Self::layout`].
82    pub fn tooltip_at(&self, mx: f32, my: f32) -> Option<&str> {
83        let path = layout::deepest_path(&self.layout_root, mx, my)?;
84        let mut w = &self.root;
85        let mut found = w.tooltip.as_deref();
86        for &i in &path {
87            let Some(c) = w.children.get(i as usize) else {
88                break;
89            };
90            w = c;
91            found = w.tooltip.as_deref().or(found);
92        }
93        found
94    }
95
96    /// Draws `text` as a vanilla tooltip next to the cursor at `(mx, my)`,
97    /// kept inside the screen. Call after [`Self::render`] so it's on top.
98    pub fn render_tooltip(ctx: &GfxContext, text: &str, mx: f32, my: f32) {
99        text::use_host_metrics(ctx);
100        let (sw, sh) = ctx.screen_size();
101        drawing::render::tooltip(&ctx.draw2d(), text, mx, my, sw as f32, sh as f32);
102    }
103
104    /// Find the deepest clickable widget at `(mx, my)` in screen coordinates.
105    pub fn hit_test(&self, mx: f32, my: f32) -> Option<&LayoutNode> {
106        layout::hit_test(&self.layout_root, mx, my)
107    }
108
109    /// Resolved rect of the (first) node tagged `id` — see [`layout::find_by_id`].
110    /// Call after [`Self::layout`].
111    pub fn find_rect(&self, id: &str) -> Option<Rect> {
112        layout::find_by_id(&self.layout_root, id)
113    }
114
115    /// Laid-out node of the (first) widget tagged `id` — rect plus scroll
116    /// state, see [`layout::find_node`]. Call after [`Self::layout`].
117    pub fn find_node(&self, id: &str) -> Option<&LayoutNode> {
118        layout::find_node(&self.layout_root, id)
119    }
120}