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::{Cx, Screen, ScreenHandle, handle_of, inventory_of};
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 /// as the game places its screens: a box bigger than the GUI keeps its
99 /// size and is cut at the edges (evenly when centred), and a
100 /// container's picture stays over its real slots (the game's
101 /// whole-pixel `(width - backgroundWidth) / 2`).
102 pub fn place(self, sw: f32, sh: f32, w: f32, h: f32) -> (f32, f32) {
103 let cx = ((sw - w) / 2.0).trunc();
104 let cy = ((sh - h) / 2.0).trunc();
105 let (x, y) = match self {
106 Anchor::Center => (cx, cy),
107 Anchor::TopLeft | Anchor::Fill => (0.0, 0.0),
108 Anchor::Top => (cx, 0.0),
109 Anchor::TopRight => (sw - w, 0.0),
110 Anchor::Left => (0.0, cy),
111 Anchor::Right => (sw - w, cy),
112 Anchor::BottomLeft => (0.0, sh - h),
113 Anchor::Bottom => (cx, sh - h),
114 Anchor::BottomRight => (sw - w, sh - h),
115 };
116 (x, y)
117 }
118}
119
120/// Static facts about a screen, from its `<Screen>` attributes.
121#[derive(Debug, Clone, Copy, PartialEq)]
122pub struct ScreenMeta {
123 /// UI id (`register_ui`, `open_ui`).
124 pub id: &'static str,
125 /// Design size in GUI pixels.
126 pub w: f32,
127 pub h: f32,
128 pub anchor: Anchor,
129 pub modal: bool,
130 pub pause: bool,
131 /// Re-evaluates the bindings every frame (`<Screen live="true">`): for
132 /// a screen showing state that changes outside its own commands (a
133 /// server tick, a packet). Inventory screens always are.
134 pub live: bool,
135}
136
137/// Input for a view's [`View::dispatch`], produced by the host.
138#[derive(Debug, Clone, PartialEq)]
139pub enum ViewEvent {
140 /// A widget with this [`Action`] was clicked or activated (Enter/Space).
141 Action(Action),
142 /// Text box `slot` (index in [`Parts`]) was edited; `text` is its text.
143 TextChanged { slot: u16, text: String },
144 /// Enter in text box `slot`.
145 TextSubmitted { slot: u16, text: String },
146 /// Keyboard selection in list `list` (its owner id) moved to `index`.
147 Select { list: u16, index: u32 },
148}
149
150/// A compiled screen — implemented by `.yui` codegen only.
151///
152/// The host owns the content tree; paths are child-index paths from its
153/// root ([`Widget::at_path`]).
154pub trait View: Sized + Send + 'static {
155 type Vm: ViewModel;
156 const META: ScreenMeta;
157 /// The view's binding state, its content tree and its interactive parts.
158 fn build() -> (Self, Widget, Parts);
159 /// Re-evaluates the bindings against `vm`, patching `root`.
160 fn update(&mut self, root: &mut Widget, vm: &Self::Vm, u: &mut Update);
161 /// Runs the command (or write-back) for `ev`.
162 fn dispatch(&mut self, vm: &mut Self::Vm, cx: &mut Cx<'_, Self::Vm>, ev: ViewEvent);
163}
164
165/// One scrolling container of a view.
166#[derive(Debug, Clone)]
167pub struct ScrollerPart {
168 pub path: Vec<u16>,
169 pub state: yog_api::ui::ScrollState,
170}
171
172/// One text box of a view.
173#[derive(Debug, Clone)]
174pub struct TextBoxPart {
175 pub path: Vec<u16>,
176 pub state: yog_api::ui::TextBoxState,
177}
178
179/// One list (ListView / ItemsControl) of a view.
180#[derive(Debug, Clone)]
181pub struct ListPart {
182 /// The list's owner id (in its items' [`Action::owner`]).
183 pub owner: u16,
184 /// The items container.
185 pub path: Vec<u16>,
186 /// Its scroller (index in [`Parts`]), for ListView.
187 pub scroller: Option<u16>,
188 /// Fixed row height (virtualization, keyboard reveal).
189 pub item_height: Option<f32>,
190 /// Item count and selected index after the last update.
191 pub len: u32,
192 pub selected: Option<u32>,
193}
194
195/// The interactive parts of a view, created by [`View::build`]: scrollers,
196/// text boxes, lists, and the Tab focus order. Indices are the order they
197/// were added.
198#[derive(Debug, Clone, Default)]
199pub struct Parts {
200 pub scrollers: Vec<ScrollerPart>,
201 pub text_boxes: Vec<TextBoxPart>,
202 pub lists: Vec<ListPart>,
203 /// Tab order: buttons, text boxes and lists, in document order.
204 pub focusables: Vec<Vec<u16>>,
205}
206
207impl Parts {
208 pub fn new() -> Self {
209 Self::default()
210 }
211 /// Adds a scroll view at `path`; returns its index.
212 pub fn scroller(&mut self, path: &[u16]) -> u16 {
213 self.scrollers.push(ScrollerPart {
214 path: path.to_vec(),
215 state: Default::default(),
216 });
217 (self.scrollers.len() - 1) as u16
218 }
219 /// Adds a text box at `path` (also focusable); returns its slot.
220 pub fn text_box(&mut self, path: &[u16]) -> u16 {
221 self.text_boxes.push(TextBoxPart {
222 path: path.to_vec(),
223 state: Default::default(),
224 });
225 self.focusables.push(path.to_vec());
226 (self.text_boxes.len() - 1) as u16
227 }
228 /// Adds the list `owner` whose items container is at `path` (focusable
229 /// when it has a scroller, i.e. is a ListView).
230 pub fn list(
231 &mut self,
232 owner: u16,
233 path: &[u16],
234 scroller: Option<u16>,
235 item_height: Option<f32>,
236 ) {
237 self.lists.push(ListPart {
238 owner,
239 path: path.to_vec(),
240 scroller,
241 item_height,
242 len: 0,
243 selected: None,
244 });
245 if scroller.is_some() {
246 self.focusables.push(path.to_vec());
247 }
248 }
249 /// Adds a focusable widget (a button) to the Tab order.
250 pub fn focusable(&mut self, path: &[u16]) {
251 self.focusables.push(path.to_vec());
252 }
253}
254
255/// Used by generated code only.
256#[doc(hidden)]
257pub mod __codegen {
258 /// The codegen version this yog-kit understands.
259 pub const CODEGEN: u32 = 2;
260
261 /// Compile-time check that generated code matches this yog-kit.
262 pub const fn require(version: u32) {
263 if version > CODEGEN {
264 panic!(
265 "this .yui code was generated for a newer yog-kit; update the yog-kit dependency"
266 );
267 }
268 if version < CODEGEN {
269 panic!("this .yui code was generated by an old yog; regenerate it with `yog build`");
270 }
271 }
272}