1use yog_api::ui::{FlexDir, LayoutNode, StateFlags, UiRoot, Units, Widget, layout, widget};
7
8mod input;
9
10use crate::screen::{self, Request};
11use crate::{Anchor, Cx, Parts, ScreenHandle, View, ViewEvent, ViewModel};
12
13const FULL: f32 = 100.0;
15const WHEEL_STEP: f32 = 16.0;
17const FALLBACK_PAGE: u32 = 5;
19pub(crate) const LOG_CLOSE: &str = "close";
21const LOG_BACK: &str = "back";
22
23#[derive(Debug, Clone, Copy, Default, PartialEq)]
25pub struct Viewport {
26 pub offset: f32,
28 pub height: f32,
30}
31
32#[derive(Debug, Default)]
34pub struct Update {
35 pub(crate) relayout: bool,
36 pub(crate) repaint: bool,
37 pub(crate) viewports: Vec<Viewport>,
38 pub(crate) text_sets: Vec<(u16, String)>,
39 pub(crate) scroll_resets: Vec<u16>,
40 pub(crate) lists: Vec<(u16, u32, Option<u32>)>,
41}
42
43impl Update {
44 pub fn new() -> Self {
46 Self::default()
47 }
48 pub fn relayout(&mut self) {
50 self.relayout = true;
51 }
52 pub fn repaint(&mut self) {
54 self.repaint = true;
55 }
56 pub fn needs_relayout(&self) -> bool {
57 self.relayout
58 }
59 pub fn needs_repaint(&self) -> bool {
60 self.repaint || self.relayout
61 }
62 pub fn viewport(&self, scroller: u16) -> Viewport {
64 self.viewports
65 .get(scroller as usize)
66 .copied()
67 .unwrap_or_default()
68 }
69 pub fn set_text_box(&mut self, slot: u16, text: &str) {
72 self.text_sets.push((slot, text.to_owned()));
73 }
74 pub fn reset_scroll(&mut self, scroller: u16) {
76 self.scroll_resets.push(scroller);
77 self.relayout();
78 }
79 pub fn list_info(&mut self, owner: u16, len: u32, selected: Option<u32>) {
81 self.lists.push((owner, len, selected));
82 }
83}
84
85pub(crate) fn find_path(w: &Widget, id: &str) -> Option<Vec<u16>> {
87 if w.id.as_deref() == Some(id) {
88 return Some(Vec::new());
89 }
90 w.children.iter().enumerate().find_map(|(i, c)| {
91 find_path(c, id).map(|mut p| {
92 p.insert(0, i as u16);
93 p
94 })
95 })
96}
97
98fn with_root(path: &[u16]) -> Vec<u16> {
99 let mut p = Vec::with_capacity(path.len() + 1);
100 p.push(0);
101 p.extend_from_slice(path);
102 p
103}
104
105pub(crate) struct Host<W: View> {
106 pub(crate) vm: W::Vm,
107 view: W,
108 pub(crate) ui: UiRoot,
110 pub(crate) parts: Parts,
111 pub(crate) size: (f32, f32),
112 pub(crate) dirty: bool,
113 hover: Option<Vec<u16>>,
115 pressed: Option<Vec<u16>>,
116 focus: Option<Vec<u16>>,
117 focus_visible: bool,
119 outlined: Option<Vec<u16>>,
120 mouse: Option<(f32, f32)>,
121 captured: Option<Vec<u16>>,
123 hovered_surface: Option<Vec<u16>>,
125 opened: bool,
126 reveal: Option<(u16, u32)>,
127 seen_viewports: Vec<Viewport>,
129 pub(crate) handle: ScreenHandle<W::Vm>,
130 pub(crate) log: Vec<String>,
132 pub(crate) live: bool,
134 pub(crate) every_frame: bool,
137}
138
139impl<W: View> Host<W> {
140 pub(crate) fn new(vm: W::Vm, handle: ScreenHandle<W::Vm>, live: bool) -> Self {
141 let (view, content, parts) = W::build();
142 let screen = widget::panel(FlexDir::Column).child(content);
143 Self {
144 vm,
145 view,
146 ui: UiRoot::new(W::META.id, screen),
147 parts,
148 size: (W::META.w, W::META.h),
149 dirty: true,
150 hover: None,
151 pressed: None,
152 focus: None,
153 focus_visible: false,
154 outlined: None,
155 mouse: None,
156 captured: None,
157 hovered_surface: None,
158 opened: false,
159 reveal: None,
160 seen_viewports: Vec::new(),
161 handle,
162 log: Vec::new(),
163 live,
164 every_frame: false,
165 }
166 }
167
168 pub(crate) fn content(&self) -> &Widget {
169 &self.ui.root.children[0]
170 }
171
172 fn node(&self, path: &[u16]) -> Option<&LayoutNode> {
173 layout::node_at(&self.ui.layout_root, &with_root(path))
174 }
175
176 #[cfg(any(test, feature = "testing"))]
177 pub(crate) fn content_node(&self, path: &[u16]) -> Option<&LayoutNode> {
178 self.node(path)
179 }
180
181 fn hit(&self, x: f32, y: f32) -> Option<Vec<u16>> {
183 let p = layout::hit_path(&self.ui.layout_root, x, y)?;
184 (p.first() == Some(&0)).then(|| p[1..].to_vec())
185 }
186
187 fn with_cx<R>(&mut self, f: impl FnOnce(&mut W::Vm, &mut W, &mut Cx<'_, W::Vm>) -> R) -> R {
190 let mut reqs = Vec::new();
191 let r = {
192 let mut cx = Cx::new(&mut reqs, &self.handle);
193 f(&mut self.vm, &mut self.view, &mut cx)
194 };
195 self.dirty = true;
196 for req in reqs {
197 self.request(req);
198 }
199 r
200 }
201
202 fn request(&mut self, req: Request) {
203 let id = W::META.id;
204 match req {
205 Request::Close => {
206 self.log.push(LOG_CLOSE.into());
207 if self.live {
208 yog_api::close_ui(id);
209 }
210 }
211 Request::Navigate(to) => {
212 self.log.push(format!("navigate:{to}"));
213 if self.live {
214 screen::navigate(id, &to);
215 }
216 }
217 Request::Back => {
218 self.log.push(LOG_BACK.into());
219 if !self.live || !screen::go_back() {
220 self.request(Request::Close);
221 }
222 }
223 Request::Focus(wid) => {
224 if let Some(p) = find_path(self.content(), &wid) {
225 self.set_focus(Some(p), true);
226 }
227 }
228 Request::Invalidate => self.dirty = true,
229 }
230 }
231
232 pub(crate) fn dispatch(&mut self, ev: ViewEvent) {
233 self.with_cx(|vm, view, cx| view.dispatch(vm, cx, ev));
234 }
235
236 fn open_vm(&mut self) {
237 self.opened = true;
238 self.reset_input();
239 self.with_cx(|vm, _, cx| vm.on_open(cx));
240 }
241
242 fn close_vm(&mut self) {
243 self.opened = false;
244 self.reset_input();
245 self.with_cx(|vm, _, cx| vm.on_close(cx));
246 }
247
248 fn reset_input(&mut self) {
249 self.hover = None;
250 self.pressed = None;
251 self.focus = None;
252 self.mouse = None;
253 for t in &mut self.parts.text_boxes {
254 t.state.blur();
255 }
256 }
257
258 pub(crate) fn drain_queue(&mut self) {
259 for job in self.handle.take_queue() {
260 job(&mut self.vm);
261 self.dirty = true;
262 }
263 }
264
265 pub(crate) fn frame(&mut self, sw: f32, sh: f32) {
269 self.size = (sw, sh);
270 self.drain_queue();
271 self.dirty |= self.every_frame;
272 for _ in 0..2 {
275 if self.dirty {
276 self.run_update();
277 }
278 self.apply_parts();
279 self.apply_states();
280 self.layout();
281 if !self.do_reveal() && !self.viewports_changed() {
282 break;
283 }
284 }
285 }
286
287 fn viewports(&self) -> Vec<Viewport> {
288 self.parts
289 .scrollers
290 .iter()
291 .map(|s| Viewport {
292 offset: s.state.offset,
293 height: self.node(&s.path).map_or(0.0, |n| n.rect.h),
294 })
295 .collect()
296 }
297
298 fn viewports_changed(&mut self) -> bool {
302 let now = self.viewports();
303 let changed = self.parts.lists.iter().any(|l| {
304 let (Some(_), Some(s)) = (l.item_height, l.scroller) else {
305 return false;
306 };
307 let (a, b) = (self.seen_viewports.get(s as usize), now.get(s as usize));
308 a.map(|v| (v.height, v.offset)) != b.map(|v| (v.height, v.offset))
309 });
310 if changed {
311 self.dirty = true;
312 }
313 changed
314 }
315
316 fn run_update(&mut self) {
317 let viewports = self.viewports();
318 self.seen_viewports.clone_from(&viewports);
319 let mut u = Update {
320 viewports,
321 ..Update::default()
322 };
323 let content = &mut self.ui.root.children[0];
324 self.view.update(content, &self.vm, &mut u);
325 self.vm.customize(content);
326 for (slot, text) in u.text_sets {
327 if let Some(t) = self.parts.text_boxes.get_mut(slot as usize)
328 && t.state.text() != text
329 {
330 t.state.set_text(text);
331 }
332 }
333 for s in u.scroll_resets {
334 if let Some(s) = self.parts.scrollers.get_mut(s as usize) {
335 s.state.reset();
336 }
337 }
338 for (owner, len, selected) in u.lists {
339 if let Some(l) = self.parts.lists.iter_mut().find(|l| l.owner == owner) {
340 l.len = len;
341 l.selected = selected;
342 }
343 }
344 self.dirty = false;
345 }
346
347 fn apply_parts(&mut self) {
348 let content = &mut self.ui.root.children[0];
349 for s in &self.parts.scrollers {
350 if let Some(w) = content.get_path_mut(&s.path) {
351 w.style.scroll_y = s.state.offset;
352 }
353 }
354 for t in &mut self.parts.text_boxes {
355 if let Some(w) = content.get_path_mut(&t.path) {
356 t.state.apply(w);
357 }
358 }
359 }
360
361 fn apply_states(&mut self) {
362 let content = &mut self.ui.root.children[0];
363 content.clear_state_deep(StateFlags::HOVER | StateFlags::PRESSED | StateFlags::FOCUSED);
364 if let Some(p) = self.outlined.take()
365 && let Some(w) = content.get_path_mut(&p)
366 {
367 w.focused = false;
368 }
369 for (path, flag) in [
370 (&self.hover, StateFlags::HOVER),
371 (&self.pressed, StateFlags::PRESSED),
372 (&self.focus, StateFlags::FOCUSED),
373 ] {
374 if let Some(w) = path.as_deref().and_then(|p| content.get_path_mut(p)) {
375 w.set_state_inherited(flag, true);
376 }
377 }
378 if self.focus_visible
379 && let Some(p) = &self.focus
380 && let Some(w) = content.get_path_mut(p)
381 && w.is_interactive()
382 {
383 w.focused = true;
384 self.outlined = Some(p.clone());
385 }
386 }
387
388 fn layout(&mut self) {
389 let (sw, sh) = self.size;
390 let m = W::META;
391 let root = &mut self.ui.root;
392 root.style.w = Units::Px(sw);
393 root.style.h = Units::Px(sh);
394 let c = &mut root.children[0];
395 if m.anchor == Anchor::Fill {
396 c.style.w = Units::Percent(FULL);
397 c.style.h = Units::Percent(FULL);
398 c.style.pos = Some((0.0, 0.0));
399 } else {
400 c.style.w = Units::Px(m.w);
401 c.style.h = Units::Px(m.h);
402 c.style.pos = Some(m.anchor.place(sw, sh, m.w, m.h));
403 }
404 self.ui.layout(sw, sh);
405 }
406
407 fn do_reveal(&mut self) -> bool {
410 let Some((owner, index)) = self.reveal.take() else {
411 return false;
412 };
413 let Some(list) = self.parts.lists.iter().find(|l| l.owner == owner) else {
414 return false;
415 };
416 let Some(si) = list.scroller else {
417 return false;
418 };
419 let Some(scroller) = self.parts.scrollers.get(si as usize) else {
420 return false;
421 };
422 let (Some(view), Some(items)) = (self.node(&scroller.path), self.node(&list.path)) else {
423 return false;
424 };
425 let found = items.children.iter().find(|c| {
426 c.action
427 .is_some_and(|a| a.owner == owner && a.index == index)
428 });
429 let (top, h) = match (found, list.item_height) {
430 (Some(n), _) => (n.rect.y - view.rect.y + view.scroll_y, n.rect.h),
431 (None, Some(ih)) => {
432 let w = self.content().get_path(&list.path);
433 let (gap, pad) = w.map_or((0.0, 0.0), |w| (w.style.gap, w.style.pad[0]));
434 (pad + index as f32 * (ih + gap), ih)
435 }
436 (None, None) => return false,
437 };
438 let (view_h, max) = (view.rect.h, view.max_scroll());
439 let state = &mut self.parts.scrollers[si as usize].state;
440 let before = state.offset;
441 state.scroll_into_view(top, h, view_h);
442 state.offset = state.offset.clamp(0.0, max.max(0.0));
443 if (state.offset - before).abs() > f32::EPSILON {
444 self.dirty = true;
445 true
446 } else {
447 false
448 }
449 }
450
451 pub(crate) fn draw_tooltip(&self, gfx: &yog_api::GfxContext) {
453 if let Some((x, y)) = self.mouse
454 && let Some(text) = self.ui.tooltip_at(x, y)
455 {
456 UiRoot::render_tooltip(gfx, text, x, y);
457 }
458 }
459}