Skip to main content

yog_kit/
lib.rs

1//! yog-kit — MVVM screens for Yog mods.
2//!
3//! A screen is a `.yui` file with `x:ViewModel="MyVm"` plus a plain Rust
4//! view model. `yog build` compiles the markup into a [`View`] (widget tree,
5//! compiled `{x:Bind ...}` bindings, command dispatch) and this crate hosts
6//! it: it owns the view model, re-evaluates bindings after every change,
7//! routes input (clicks, hover, scrolling, text boxes, Tab focus, list
8//! keyboard selection, Esc) and draws the result.
9//!
10//! ```ignore
11//! // src/counter.rs, next to src/counter.yui
12//! yog_api::ui_include!("counter");
13//! use yog_kit::prelude::*;
14//!
15//! #[derive(Default)]
16//! pub struct CounterVm { count: u32 }
17//! impl ViewModel for CounterVm {}
18//! impl CounterVm {
19//!     pub fn increment(&mut self) { self.count += 1 }
20//! }
21//!
22//! // src/lib.rs: counter::register(registry); ... counter::open();
23//! ```
24//!
25//! The view model never touches widgets: the markup binds to its fields and
26//! methods, and commands call its methods. [`testing::TestHost`] drives a
27//! screen without the game.
28
29extern crate self as yog_kit;
30
31pub mod bind;
32mod host;
33pub mod items;
34mod screen;
35pub mod testing;
36
37pub use host::{Update, Viewport};
38pub use screen::{handle_of, Cx, Screen, ScreenHandle};
39pub use yog_api::ui::{Action, StateFlags, Widget};
40
41/// Everything a code-behind file needs.
42pub mod prelude {
43    pub use crate::{Cx, ScreenHandle, ViewModel};
44}
45
46/// A screen's state and logic. Implement it (often with no methods) for the
47/// type named by the `.yui` file's `x:ViewModel`.
48///
49/// All hooks run on the render thread with the screen locked; use
50/// [`Cx`] to close the screen, navigate, or get a [`ScreenHandle`] for
51/// updates from other threads.
52#[allow(unused_variables)]
53pub trait ViewModel: Send + 'static {
54    /// The screen was opened (every time).
55    fn on_open(&mut self, cx: &mut Cx<'_, Self>)
56    where
57        Self: Sized,
58    {
59    }
60    /// The screen was closed (by any path).
61    fn on_close(&mut self, cx: &mut Cx<'_, Self>)
62    where
63        Self: Sized,
64    {
65    }
66    /// Esc / back. Return `true` when handled; `false` (the default) goes
67    /// back to the screen this one was navigated from, else closes it.
68    fn on_back(&mut self, cx: &mut Cx<'_, Self>) -> bool
69    where
70        Self: Sized,
71    {
72        false
73    }
74    /// Escape hatch: imperative changes to the content tree after each
75    /// binding update. Rarely needed.
76    fn customize(&self, root: &mut Widget) {}
77}
78
79/// Where a screen's content sits in the GUI.
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
81pub enum Anchor {
82    #[default]
83    Center,
84    TopLeft,
85    Top,
86    TopRight,
87    Left,
88    Right,
89    BottomLeft,
90    Bottom,
91    BottomRight,
92    /// Stretched over the whole GUI.
93    Fill,
94}
95
96impl Anchor {
97    /// Top-left corner of a `w` × `h` box anchored in a `sw` × `sh` GUI.
98    pub fn place(self, sw: f32, sh: f32, w: f32, h: f32) -> (f32, f32) {
99        let cx = ((sw - w) / 2.0).floor();
100        let cy = ((sh - h) / 2.0).floor();
101        let (x, y) = match self {
102            Anchor::Center => (cx, cy),
103            Anchor::TopLeft | Anchor::Fill => (0.0, 0.0),
104            Anchor::Top => (cx, 0.0),
105            Anchor::TopRight => (sw - w, 0.0),
106            Anchor::Left => (0.0, cy),
107            Anchor::Right => (sw - w, cy),
108            Anchor::BottomLeft => (0.0, sh - h),
109            Anchor::Bottom => (cx, sh - h),
110            Anchor::BottomRight => (sw - w, sh - h),
111        };
112        (x.max(0.0), y.max(0.0))
113    }
114}
115
116/// Static facts about a screen, from its `<Screen>` attributes.
117#[derive(Debug, Clone, Copy, PartialEq)]
118pub struct ScreenMeta {
119    /// UI id (`register_ui`, `open_ui`).
120    pub id: &'static str,
121    /// Design size in GUI pixels.
122    pub w: f32,
123    pub h: f32,
124    pub anchor: Anchor,
125    pub modal: bool,
126    pub pause: bool,
127}
128
129/// Input for a view's [`View::dispatch`], produced by the host.
130#[derive(Debug, Clone, PartialEq)]
131pub enum ViewEvent {
132    /// A widget with this [`Action`] was clicked or activated (Enter/Space).
133    Action(Action),
134    /// Text box `slot` (index in [`Parts`]) was edited; `text` is its text.
135    TextChanged { slot: u16, text: String },
136    /// Enter in text box `slot`.
137    TextSubmitted { slot: u16, text: String },
138    /// Keyboard selection in list `list` (its owner id) moved to `index`.
139    Select { list: u16, index: u32 },
140}
141
142/// A compiled screen — implemented by `.yui` codegen only.
143///
144/// The host owns the content tree; paths are child-index paths from its
145/// root ([`Widget::at_path`]).
146pub trait View: Sized + Send + 'static {
147    type Vm: ViewModel;
148    const META: ScreenMeta;
149    /// The view's binding state, its content tree and its interactive parts.
150    fn build() -> (Self, Widget, Parts);
151    /// Re-evaluates the bindings against `vm`, patching `root`.
152    fn update(&mut self, root: &mut Widget, vm: &Self::Vm, u: &mut Update);
153    /// Runs the command (or write-back) for `ev`.
154    fn dispatch(&mut self, vm: &mut Self::Vm, cx: &mut Cx<'_, Self::Vm>, ev: ViewEvent);
155}
156
157/// One scrolling container of a view.
158#[derive(Debug, Clone)]
159pub struct ScrollerPart {
160    pub path: Vec<u16>,
161    pub state: yog_api::ui::ScrollState,
162}
163
164/// One text box of a view.
165#[derive(Debug, Clone)]
166pub struct TextBoxPart {
167    pub path: Vec<u16>,
168    pub state: yog_api::ui::TextBoxState,
169}
170
171/// One list (ListView / ItemsControl) of a view.
172#[derive(Debug, Clone)]
173pub struct ListPart {
174    /// The list's owner id (in its items' [`Action::owner`]).
175    pub owner: u16,
176    /// The items container.
177    pub path: Vec<u16>,
178    /// Its scroller (index in [`Parts`]), for ListView.
179    pub scroller: Option<u16>,
180    /// Fixed row height (virtualization, keyboard reveal).
181    pub item_height: Option<f32>,
182    /// Item count and selected index after the last update.
183    pub len: u32,
184    pub selected: Option<u32>,
185}
186
187/// The interactive parts of a view, created by [`View::build`]: scrollers,
188/// text boxes, lists, and the Tab focus order. Indices are the order they
189/// were added.
190#[derive(Debug, Clone, Default)]
191pub struct Parts {
192    pub scrollers: Vec<ScrollerPart>,
193    pub text_boxes: Vec<TextBoxPart>,
194    pub lists: Vec<ListPart>,
195    /// Tab order: buttons, text boxes and lists, in document order.
196    pub focusables: Vec<Vec<u16>>,
197}
198
199impl Parts {
200    pub fn new() -> Self {
201        Self::default()
202    }
203    /// Adds a scroll view at `path`; returns its index.
204    pub fn scroller(&mut self, path: &[u16]) -> u16 {
205        self.scrollers.push(ScrollerPart {
206            path: path.to_vec(),
207            state: Default::default(),
208        });
209        (self.scrollers.len() - 1) as u16
210    }
211    /// Adds a text box at `path` (also focusable); returns its slot.
212    pub fn text_box(&mut self, path: &[u16]) -> u16 {
213        self.text_boxes.push(TextBoxPart {
214            path: path.to_vec(),
215            state: Default::default(),
216        });
217        self.focusables.push(path.to_vec());
218        (self.text_boxes.len() - 1) as u16
219    }
220    /// Adds the list `owner` whose items container is at `path` (focusable
221    /// when it has a scroller, i.e. is a ListView).
222    pub fn list(&mut self, owner: u16, path: &[u16], scroller: Option<u16>, item_height: Option<f32>) {
223        self.lists.push(ListPart {
224            owner,
225            path: path.to_vec(),
226            scroller,
227            item_height,
228            len: 0,
229            selected: None,
230        });
231        if scroller.is_some() {
232            self.focusables.push(path.to_vec());
233        }
234    }
235    /// Adds a focusable widget (a button) to the Tab order.
236    pub fn focusable(&mut self, path: &[u16]) {
237        self.focusables.push(path.to_vec());
238    }
239}
240
241/// Used by generated code only.
242#[doc(hidden)]
243pub mod __codegen {
244    /// The codegen version this yog-kit understands.
245    pub const CODEGEN: u32 = 1;
246
247    /// Compile-time check that generated code matches this yog-kit.
248    pub const fn require(version: u32) {
249        if version > CODEGEN {
250            panic!("this .yui code was generated for a newer yog-kit; update the yog-kit dependency");
251        }
252        if version < 1 {
253            panic!("this .yui code was generated by an old yog; regenerate it with `yog build`");
254        }
255    }
256}