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;
35/// Driving a screen in unit tests: a mod's tests turn it on with the
36/// `testing` feature (`yog build` does, in their `[dev-dependencies]`).
37#[cfg(any(test, feature = "testing"))]
38pub mod testing;
39
40pub use host::{Update, Viewport};
41pub use screen::{Cx, Screen, ScreenHandle, handle_of, inventory_of};
42pub use yog_api::ui::{Action, StateFlags, Widget};
43
44/// Everything a code-behind file needs.
45pub mod prelude {
46    pub use crate::{Cx, ScreenHandle, ViewModel};
47}
48
49/// A screen's state and logic. Implement it (often with no methods) for the
50/// type named by the `.yui` file's `x:ViewModel`.
51///
52/// All hooks run on the render thread with the screen locked; use
53/// [`Cx`] to close the screen, navigate, or get a [`ScreenHandle`] for
54/// updates from other threads.
55#[allow(unused_variables)]
56pub trait ViewModel: Send + 'static {
57    /// The screen was opened (every time).
58    fn on_open(&mut self, cx: &mut Cx<'_, Self>)
59    where
60        Self: Sized,
61    {
62    }
63    /// The screen was closed (by any path).
64    fn on_close(&mut self, cx: &mut Cx<'_, Self>)
65    where
66        Self: Sized,
67    {
68    }
69    /// Esc / back. Return `true` when handled; `false` (the default) goes
70    /// back to the screen this one was navigated from, else closes it.
71    fn on_back(&mut self, cx: &mut Cx<'_, Self>) -> bool
72    where
73        Self: Sized,
74    {
75        false
76    }
77    /// Escape hatch: imperative changes to the content tree after each
78    /// binding update. Rarely needed.
79    fn customize(&self, root: &mut Widget) {}
80}
81
82/// Where a screen's content sits in the GUI.
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
84pub enum Anchor {
85    #[default]
86    Center,
87    TopLeft,
88    Top,
89    TopRight,
90    Left,
91    Right,
92    BottomLeft,
93    Bottom,
94    BottomRight,
95    /// Stretched over the whole GUI.
96    Fill,
97}
98
99impl Anchor {
100    /// Top-left corner of a `w` × `h` box anchored in a `sw` × `sh` GUI —
101    /// as the game places its screens: a box bigger than the GUI keeps its
102    /// size and is cut at the edges (evenly when centred), and a
103    /// container's picture stays over its real slots (the game's
104    /// whole-pixel `(width - backgroundWidth) / 2`).
105    pub fn place(self, sw: f32, sh: f32, w: f32, h: f32) -> (f32, f32) {
106        let cx = ((sw - w) / 2.0).trunc();
107        let cy = ((sh - h) / 2.0).trunc();
108        let (x, y) = match self {
109            Anchor::Center => (cx, cy),
110            Anchor::TopLeft | Anchor::Fill => (0.0, 0.0),
111            Anchor::Top => (cx, 0.0),
112            Anchor::TopRight => (sw - w, 0.0),
113            Anchor::Left => (0.0, cy),
114            Anchor::Right => (sw - w, cy),
115            Anchor::BottomLeft => (0.0, sh - h),
116            Anchor::Bottom => (cx, sh - h),
117            Anchor::BottomRight => (sw - w, sh - h),
118        };
119        (x, y)
120    }
121}
122
123/// Static facts about a screen, from its `<Screen>` attributes.
124#[derive(Debug, Clone, Copy, PartialEq)]
125pub struct ScreenMeta {
126    /// UI id (`register_ui`, `open_ui`).
127    pub id: &'static str,
128    /// Design size in GUI pixels.
129    pub w: f32,
130    pub h: f32,
131    pub anchor: Anchor,
132    pub modal: bool,
133    pub pause: bool,
134    /// Re-evaluates the bindings every frame (`<Screen live="true">`): for
135    /// a screen showing state that changes outside its own commands (a
136    /// server tick, a packet). Inventory screens always are.
137    pub live: bool,
138}
139
140/// Input for a view's [`View::dispatch`], produced by the host.
141#[derive(Debug, Clone, PartialEq)]
142pub enum ViewEvent {
143    /// A widget with this [`Action`] was clicked or activated (Enter/Space).
144    Action(Action),
145    /// Text box `slot` (index in [`Parts`]) was edited; `text` is its text.
146    TextChanged { slot: u16, text: String },
147    /// Enter in text box `slot`.
148    TextSubmitted { slot: u16, text: String },
149    /// Keyboard selection in list `list` (its owner id) moved to `index`.
150    Select { list: u16, index: u32 },
151}
152
153/// A compiled screen — implemented by `.yui` codegen only.
154///
155/// The host owns the content tree; paths are child-index paths from its
156/// root ([`Widget::get_path`]).
157pub trait View: Sized + Send + 'static {
158    type Vm: ViewModel;
159    const META: ScreenMeta;
160    /// The view's binding state, its content tree and its interactive parts.
161    fn build() -> (Self, Widget, Parts);
162    /// Re-evaluates the bindings against `vm`, patching `root`.
163    fn update(&mut self, root: &mut Widget, vm: &Self::Vm, u: &mut Update);
164    /// Runs the command (or write-back) for `ev`.
165    fn dispatch(&mut self, vm: &mut Self::Vm, cx: &mut Cx<'_, Self::Vm>, ev: ViewEvent);
166}
167
168/// One scrolling container of a view.
169#[derive(Debug, Clone)]
170pub struct ScrollerPart {
171    pub path: Vec<u16>,
172    pub state: yog_api::ui::ScrollState,
173}
174
175/// One text box of a view.
176#[derive(Debug, Clone)]
177pub struct TextBoxPart {
178    pub path: Vec<u16>,
179    pub state: yog_api::ui::TextBoxState,
180}
181
182/// One list (ListView / ItemsControl) of a view.
183#[derive(Debug, Clone)]
184pub struct ListPart {
185    /// The list's owner id (in its items' [`Action::owner`]).
186    pub owner: u16,
187    /// The items container.
188    pub path: Vec<u16>,
189    /// Its scroller (index in [`Parts`]), for ListView.
190    pub scroller: Option<u16>,
191    /// Fixed row height (virtualization, keyboard reveal).
192    pub item_height: Option<f32>,
193    /// Item count and selected index after the last update.
194    pub len: u32,
195    pub selected: Option<u32>,
196}
197
198/// The interactive parts of a view, created by [`View::build`]: scrollers,
199/// text boxes, lists, and the Tab focus order. Indices are the order they
200/// were added.
201#[derive(Debug, Clone, Default)]
202pub struct Parts {
203    pub scrollers: Vec<ScrollerPart>,
204    pub text_boxes: Vec<TextBoxPart>,
205    pub lists: Vec<ListPart>,
206    /// Tab order: buttons, text boxes and lists, in document order.
207    pub focusables: Vec<Vec<u16>>,
208}
209
210impl Parts {
211    pub fn new() -> Self {
212        Self::default()
213    }
214    /// Adds a scroll view at `path`; returns its index.
215    pub fn scroller(&mut self, path: &[u16]) -> u16 {
216        self.scrollers.push(ScrollerPart {
217            path: path.to_vec(),
218            state: Default::default(),
219        });
220        (self.scrollers.len() - 1) as u16
221    }
222    /// Adds a text box at `path` (also focusable); returns its slot.
223    pub fn text_box(&mut self, path: &[u16]) -> u16 {
224        self.text_boxes.push(TextBoxPart {
225            path: path.to_vec(),
226            state: Default::default(),
227        });
228        self.focusables.push(path.to_vec());
229        (self.text_boxes.len() - 1) as u16
230    }
231    /// Adds the list `owner` whose items container is at `path` (focusable
232    /// when it has a scroller, i.e. is a ListView).
233    pub fn list(
234        &mut self,
235        owner: u16,
236        path: &[u16],
237        scroller: Option<u16>,
238        item_height: Option<f32>,
239    ) {
240        self.lists.push(ListPart {
241            owner,
242            path: path.to_vec(),
243            scroller,
244            item_height,
245            len: 0,
246            selected: None,
247        });
248        if scroller.is_some() {
249            self.focusables.push(path.to_vec());
250        }
251    }
252    /// Adds a focusable widget (a button) to the Tab order.
253    pub fn focusable(&mut self, path: &[u16]) {
254        self.focusables.push(path.to_vec());
255    }
256}
257
258/// Used by generated code only.
259#[doc(hidden)]
260pub mod __codegen {
261    /// The codegen version this yog-kit understands.
262    pub const CODEGEN: u32 = yog_api::yog_ids::versions::UI_CODEGEN;
263
264    /// Compile-time check that generated code matches this yog-kit
265    /// (`const _: () = require(N);`: a mismatch fails the mod's build).
266    pub const fn require(version: u32) {
267        assert!(
268            version <= CODEGEN,
269            "this .yui code was generated for a newer yog-kit; update the yog-kit dependency"
270        );
271        assert!(
272            version >= CODEGEN,
273            "this .yui code was generated by an old yog; regenerate it with `yog build`"
274        );
275    }
276}