1use yog_api::ui::event::keys;
7use yog_api::ui::{
8 layout, widget, Action, FlexDir, LayoutNode, StateFlags, TextBoxEvent, UiEvent, UiRoot, Units,
9 Widget,
10};
11
12use crate::screen::{self, Request};
13use crate::{Anchor, Cx, Parts, ScreenHandle, View, ViewEvent, ViewModel};
14
15const WHEEL_STEP: f32 = 16.0;
17
18#[derive(Debug, Clone, Copy, Default, PartialEq)]
20pub struct Viewport {
21 pub offset: f32,
23 pub height: f32,
25}
26
27#[derive(Debug, Default)]
29pub struct Update {
30 pub(crate) relayout: bool,
31 pub(crate) repaint: bool,
32 pub(crate) viewports: Vec<Viewport>,
33 pub(crate) text_sets: Vec<(u16, String)>,
34 pub(crate) scroll_resets: Vec<u16>,
35 pub(crate) lists: Vec<(u16, u32, Option<u32>)>,
36}
37
38impl Update {
39 pub fn new() -> Self {
41 Self::default()
42 }
43 pub fn relayout(&mut self) {
45 self.relayout = true;
46 }
47 pub fn repaint(&mut self) {
49 self.repaint = true;
50 }
51 pub fn needs_relayout(&self) -> bool {
52 self.relayout
53 }
54 pub fn needs_repaint(&self) -> bool {
55 self.repaint || self.relayout
56 }
57 pub fn viewport(&self, scroller: u16) -> Viewport {
59 self.viewports
60 .get(scroller as usize)
61 .copied()
62 .unwrap_or_default()
63 }
64 pub fn set_text_box(&mut self, slot: u16, text: &str) {
67 self.text_sets.push((slot, text.to_owned()));
68 }
69 pub fn reset_scroll(&mut self, scroller: u16) {
71 self.scroll_resets.push(scroller);
72 self.relayout();
73 }
74 pub fn list_info(&mut self, owner: u16, len: u32, selected: Option<u32>) {
76 self.lists.push((owner, len, selected));
77 }
78}
79
80pub(crate) fn find_path(w: &Widget, id: &str) -> Option<Vec<u16>> {
82 if w.id.as_deref() == Some(id) {
83 return Some(Vec::new());
84 }
85 w.children.iter().enumerate().find_map(|(i, c)| {
86 find_path(c, id).map(|mut p| {
87 p.insert(0, i as u16);
88 p
89 })
90 })
91}
92
93fn with_root(path: &[u16]) -> Vec<u16> {
94 let mut p = Vec::with_capacity(path.len() + 1);
95 p.push(0);
96 p.extend_from_slice(path);
97 p
98}
99
100pub(crate) struct Host<W: View> {
101 pub(crate) vm: W::Vm,
102 view: W,
103 pub(crate) ui: UiRoot,
105 pub(crate) parts: Parts,
106 pub(crate) size: (f32, f32),
107 pub(crate) dirty: bool,
108 hover: Option<Vec<u16>>,
110 pressed: Option<Vec<u16>>,
111 focus: Option<Vec<u16>>,
112 focus_visible: bool,
114 outlined: Option<Vec<u16>>,
115 mouse: Option<(f32, f32)>,
116 opened: bool,
117 reveal: Option<(u16, u32)>,
118 seen_viewports: Vec<Viewport>,
120 pub(crate) handle: ScreenHandle<W::Vm>,
121 pub(crate) log: Vec<String>,
123 pub(crate) live: bool,
125}
126
127impl<W: View> Host<W> {
128 pub(crate) fn new(vm: W::Vm, handle: ScreenHandle<W::Vm>, live: bool) -> Self {
129 let (view, content, parts) = W::build();
130 let screen = widget::panel(FlexDir::Column).child(content);
131 Self {
132 vm,
133 view,
134 ui: UiRoot::new(W::META.id, screen),
135 parts,
136 size: (W::META.w, W::META.h),
137 dirty: true,
138 hover: None,
139 pressed: None,
140 focus: None,
141 focus_visible: false,
142 outlined: None,
143 mouse: None,
144 opened: false,
145 reveal: None,
146 seen_viewports: Vec::new(),
147 handle,
148 log: Vec::new(),
149 live,
150 }
151 }
152
153 pub(crate) fn content(&self) -> &Widget {
154 &self.ui.root.children[0]
155 }
156
157 fn node(&self, path: &[u16]) -> Option<&LayoutNode> {
158 layout::node_at(&self.ui.layout_root, &with_root(path))
159 }
160
161 pub(crate) fn content_node(&self, path: &[u16]) -> Option<&LayoutNode> {
162 self.node(path)
163 }
164
165 fn hit(&self, x: f32, y: f32) -> Option<Vec<u16>> {
167 let p = layout::hit_path(&self.ui.layout_root, x, y)?;
168 (p.first() == Some(&0)).then(|| p[1..].to_vec())
169 }
170
171 fn with_cx<R>(&mut self, f: impl FnOnce(&mut W::Vm, &mut W, &mut Cx<'_, W::Vm>) -> R) -> R {
174 let mut reqs = Vec::new();
175 let r = {
176 let mut cx = Cx::new(&mut reqs, &self.handle);
177 f(&mut self.vm, &mut self.view, &mut cx)
178 };
179 self.dirty = true;
180 for req in reqs {
181 self.request(req);
182 }
183 r
184 }
185
186 fn request(&mut self, req: Request) {
187 let id = W::META.id;
188 match req {
189 Request::Close => {
190 self.log.push("close".into());
191 if self.live {
192 yog_api::close_ui(id);
193 }
194 }
195 Request::Navigate(to) => {
196 self.log.push(format!("navigate:{to}"));
197 if self.live {
198 screen::navigate(id, &to);
199 }
200 }
201 Request::Back => {
202 self.log.push("back".into());
203 if !self.live || !screen::go_back() {
204 self.request(Request::Close);
205 }
206 }
207 Request::Focus(wid) => {
208 if let Some(p) = find_path(self.content(), &wid) {
209 self.set_focus(Some(p), true);
210 }
211 }
212 Request::Invalidate => self.dirty = true,
213 }
214 }
215
216 pub(crate) fn dispatch(&mut self, ev: ViewEvent) {
217 self.with_cx(|vm, view, cx| view.dispatch(vm, cx, ev));
218 }
219
220 fn open_vm(&mut self) {
221 self.opened = true;
222 self.reset_input();
223 self.with_cx(|vm, _, cx| vm.on_open(cx));
224 }
225
226 fn close_vm(&mut self) {
227 self.opened = false;
228 self.reset_input();
229 self.with_cx(|vm, _, cx| vm.on_close(cx));
230 }
231
232 fn reset_input(&mut self) {
233 self.hover = None;
234 self.pressed = None;
235 self.focus = None;
236 self.mouse = None;
237 for t in &mut self.parts.text_boxes {
238 t.state.blur();
239 }
240 }
241
242 pub(crate) fn drain_queue(&mut self) {
243 for job in self.handle.take_queue() {
244 job(&mut self.vm);
245 self.dirty = true;
246 }
247 }
248
249 pub(crate) fn frame(&mut self, sw: f32, sh: f32) {
253 self.size = (sw, sh);
254 self.drain_queue();
255 for _ in 0..2 {
258 if self.dirty {
259 self.run_update();
260 }
261 self.apply_parts();
262 self.apply_states();
263 self.layout();
264 if !self.do_reveal() && !self.viewports_changed() {
265 break;
266 }
267 }
268 }
269
270 fn viewports(&self) -> Vec<Viewport> {
271 self.parts
272 .scrollers
273 .iter()
274 .map(|s| Viewport {
275 offset: s.state.offset,
276 height: self.node(&s.path).map_or(0.0, |n| n.rect.h),
277 })
278 .collect()
279 }
280
281 fn viewports_changed(&mut self) -> bool {
284 let now = self.viewports();
285 let changed = self.parts.lists.iter().any(|l| {
286 let (Some(_), Some(s)) = (l.item_height, l.scroller) else {
287 return false;
288 };
289 let (a, b) = (self.seen_viewports.get(s as usize), now.get(s as usize));
290 a.map(|v| v.height) != b.map(|v| v.height)
291 });
292 if changed {
293 self.dirty = true;
294 }
295 changed
296 }
297
298 fn run_update(&mut self) {
299 let viewports = self.viewports();
300 self.seen_viewports.clone_from(&viewports);
301 let mut u = Update {
302 viewports,
303 ..Update::default()
304 };
305 let content = &mut self.ui.root.children[0];
306 self.view.update(content, &self.vm, &mut u);
307 self.vm.customize(content);
308 for (slot, text) in u.text_sets {
309 if let Some(t) = self.parts.text_boxes.get_mut(slot as usize) {
310 if t.state.text() != text {
311 t.state.set_text(text);
312 }
313 }
314 }
315 for s in u.scroll_resets {
316 if let Some(s) = self.parts.scrollers.get_mut(s as usize) {
317 s.state.reset();
318 }
319 }
320 for (owner, len, selected) in u.lists {
321 if let Some(l) = self.parts.lists.iter_mut().find(|l| l.owner == owner) {
322 l.len = len;
323 l.selected = selected;
324 }
325 }
326 self.dirty = false;
327 }
328
329 fn apply_parts(&mut self) {
330 let content = &mut self.ui.root.children[0];
331 for s in &self.parts.scrollers {
332 if let Some(w) = content.get_path_mut(&s.path) {
333 w.style.scroll_y = s.state.offset;
334 }
335 }
336 for t in &mut self.parts.text_boxes {
337 if let Some(w) = content.get_path_mut(&t.path) {
338 t.state.apply(w);
339 }
340 }
341 }
342
343 fn apply_states(&mut self) {
344 let content = &mut self.ui.root.children[0];
345 content.clear_state_deep(StateFlags::HOVER | StateFlags::PRESSED | StateFlags::FOCUSED);
346 if let Some(p) = self.outlined.take() {
347 if let Some(w) = content.get_path_mut(&p) {
348 w.focused = false;
349 }
350 }
351 for (path, flag) in [
352 (&self.hover, StateFlags::HOVER),
353 (&self.pressed, StateFlags::PRESSED),
354 (&self.focus, StateFlags::FOCUSED),
355 ] {
356 if let Some(w) = path.as_deref().and_then(|p| content.get_path_mut(p)) {
357 w.set_state_inherited(flag, true);
358 }
359 }
360 if self.focus_visible {
361 if let Some(p) = &self.focus {
362 if let Some(w) = content.get_path_mut(p) {
363 if w.is_interactive() {
364 w.focused = true;
365 self.outlined = Some(p.clone());
366 }
367 }
368 }
369 }
370 }
371
372 fn layout(&mut self) {
373 let (sw, sh) = self.size;
374 let m = W::META;
375 let root = &mut self.ui.root;
376 root.style.w = Units::Px(sw);
377 root.style.h = Units::Px(sh);
378 let c = &mut root.children[0];
379 if m.anchor == Anchor::Fill {
380 c.style.w = Units::Percent(100.0);
381 c.style.h = Units::Percent(100.0);
382 c.style.pos = Some((0.0, 0.0));
383 } else {
384 c.style.w = Units::Px(m.w);
385 c.style.h = Units::Px(m.h);
386 c.style.pos = Some(m.anchor.place(sw, sh, m.w, m.h));
387 }
388 self.ui.layout(sw, sh);
389 }
390
391 fn do_reveal(&mut self) -> bool {
394 let Some((owner, index)) = self.reveal.take() else {
395 return false;
396 };
397 let Some(list) = self.parts.lists.iter().find(|l| l.owner == owner) else {
398 return false;
399 };
400 let Some(si) = list.scroller else {
401 return false;
402 };
403 let Some(scroller) = self.parts.scrollers.get(si as usize) else {
404 return false;
405 };
406 let (Some(view), Some(items)) = (self.node(&scroller.path), self.node(&list.path)) else {
407 return false;
408 };
409 let found = items
410 .children
411 .iter()
412 .find(|c| c.action.is_some_and(|a| a.owner == owner && a.index == index));
413 let (top, h) = match (found, list.item_height) {
414 (Some(n), _) => (n.rect.y - view.rect.y + view.scroll_y, n.rect.h),
415 (None, Some(ih)) => {
416 let w = self.content().get_path(&list.path);
417 let (gap, pad) = w.map_or((0.0, 0.0), |w| (w.style.gap, w.style.pad[0]));
418 (pad + index as f32 * (ih + gap), ih)
419 }
420 (None, None) => return false,
421 };
422 let (view_h, max) = (view.rect.h, view.max_scroll());
423 let state = &mut self.parts.scrollers[si as usize].state;
424 let before = state.offset;
425 state.scroll_into_view(top, h, view_h);
426 state.offset = state.offset.clamp(0.0, max.max(0.0));
427 if (state.offset - before).abs() > f32::EPSILON {
428 self.dirty = true;
429 true
430 } else {
431 false
432 }
433 }
434
435 pub(crate) fn on_event(&mut self, raw: &str) -> bool {
439 self.drain_queue();
440 let ev = UiEvent::parse(raw);
441 match ev {
442 UiEvent::Open { w, h } => {
443 self.size = (w, h);
444 if self.opened {
445 self.close_vm();
446 }
447 self.open_vm();
448 false
449 }
450 UiEvent::Close => {
451 if self.opened {
452 self.close_vm();
453 }
454 if self.live {
455 screen::screen_closed();
456 }
457 false
458 }
459 UiEvent::Move { x, y } => {
460 self.mouse = Some((x, y));
461 self.hover = self.hit(x, y);
462 false
463 }
464 UiEvent::Scroll { .. } => self.scroll(&ev),
465 UiEvent::Click { x, y } => self.click(x, y, &ev),
466 UiEvent::Drag { .. } | UiEvent::Release { .. } => {
467 if matches!(ev, UiEvent::Release { .. }) {
468 self.pressed = None;
469 }
470 let mut used = false;
471 for i in 0..self.parts.scrollers.len() {
472 let Some(node) = self.node(&self.parts.scrollers[i].path).cloned() else {
473 continue;
474 };
475 if self.parts.scrollers[i].state.handle_event(&ev, &node, WHEEL_STEP) {
476 used = true;
477 self.dirty = true;
478 }
479 }
480 for i in 0..self.parts.text_boxes.len() {
481 let Some(node) = self.node(&self.parts.text_boxes[i].path).cloned() else {
482 continue;
483 };
484 used |= self.parts.text_boxes[i]
485 .state
486 .handle_event(&ev, &node)
487 .consumed();
488 }
489 used
490 }
491 UiEvent::Key { key, mods } => self.key(key, mods, &ev),
492 UiEvent::Char(_) => self.text_input(&ev),
493 UiEvent::Other(_) => false,
494 }
495 }
496
497 fn scroll(&mut self, ev: &UiEvent<'_>) -> bool {
498 let under = self
499 .mouse
500 .and_then(|(x, y)| layout::deepest_path(&self.ui.layout_root, x, y))
501 .filter(|p| p.first() == Some(&0))
502 .map(|p| p[1..].to_vec());
503 let target = under
504 .and_then(|p| {
505 self.parts
506 .scrollers
507 .iter()
508 .enumerate()
509 .filter(|(_, s)| p.starts_with(&s.path))
510 .max_by_key(|(_, s)| s.path.len())
511 .map(|(i, _)| i)
512 })
513 .or_else(|| self.focused_list().and_then(|l| l.scroller.map(usize::from)))
514 .or_else(|| (!self.parts.scrollers.is_empty()).then_some(0));
515 let Some(i) = target else {
516 return false;
517 };
518 let Some(node) = self.node(&self.parts.scrollers[i].path).cloned() else {
519 return false;
520 };
521 self.parts.scrollers[i]
522 .state
523 .handle_event(ev, &node, WHEEL_STEP);
524 self.dirty = true;
525 true
526 }
527
528 fn click(&mut self, x: f32, y: f32, ev: &UiEvent<'_>) -> bool {
529 self.mouse = Some((x, y));
530 self.hover = self.hit(x, y);
531 for i in 0..self.parts.scrollers.len() {
532 let Some(node) = self.node(&self.parts.scrollers[i].path).cloned() else {
533 continue;
534 };
535 if self.parts.scrollers[i].state.handle_event(ev, &node, WHEEL_STEP) {
536 self.dirty = true;
537 return true;
538 }
539 }
540 let mut clicked_box = None;
541 for i in 0..self.parts.text_boxes.len() {
542 let Some(node) = self.node(&self.parts.text_boxes[i].path).cloned() else {
543 continue;
544 };
545 if self.parts.text_boxes[i]
546 .state
547 .handle_event(ev, &node)
548 .consumed()
549 {
550 clicked_box = Some(i);
551 }
552 }
553 if let Some(i) = clicked_box {
554 self.focus = Some(self.parts.text_boxes[i].path.clone());
555 self.focus_visible = false;
556 return true;
557 }
558 let Some(path) = self.hit(x, y) else {
559 self.focus = None;
560 return false;
561 };
562 self.pressed = Some(path.clone());
563 self.focus = self
564 .parts
565 .lists
566 .iter()
567 .filter(|l| l.scroller.is_some() && path.starts_with(&l.path))
568 .max_by_key(|l| l.path.len())
569 .map(|l| l.path.clone());
570 self.focus_visible = false;
571 self.activate(&path);
572 true
573 }
574
575 fn activate(&mut self, path: &[u16]) {
577 let Some(w) = self.content().get_path(path) else {
578 return;
579 };
580 if !w.enabled || !w.visible {
581 return;
582 }
583 if let Some(a) = w.action {
584 if a.slot == Action::SELECT {
585 self.reveal = Some((a.owner, a.index));
586 }
587 self.dispatch(ViewEvent::Action(a));
588 }
589 }
590
591 fn focused_box(&self) -> Option<usize> {
592 self.parts
593 .text_boxes
594 .iter()
595 .position(|t| t.state.is_focused())
596 }
597
598 fn focused_list(&self) -> Option<&crate::ListPart> {
599 let f = self.focus.as_ref()?;
600 self.parts.lists.iter().find(|l| &l.path == f)
601 }
602
603 fn text_input(&mut self, ev: &UiEvent<'_>) -> bool {
605 let Some(i) = self.focused_box() else {
606 return false;
607 };
608 let Some(node) = self.node(&self.parts.text_boxes[i].path).cloned() else {
609 return false;
610 };
611 let r = self.parts.text_boxes[i].state.handle_event(ev, &node);
612 let text = || self.parts.text_boxes[i].state.text().to_owned();
613 match r {
614 TextBoxEvent::Changed => {
615 let text = text();
616 self.dispatch(ViewEvent::TextChanged {
617 slot: i as u16,
618 text,
619 });
620 }
621 TextBoxEvent::Submitted => {
622 let text = text();
623 self.dispatch(ViewEvent::TextSubmitted {
624 slot: i as u16,
625 text,
626 });
627 }
628 _ => {}
629 }
630 if !self.parts.text_boxes[i].state.is_focused() {
631 self.focus = None;
632 }
633 r.consumed()
634 }
635
636 fn key(&mut self, key: i32, mods: i32, ev: &UiEvent<'_>) -> bool {
637 if key == keys::TAB {
638 self.cycle_focus(mods & keys::MOD_SHIFT != 0);
639 return true;
640 }
641 if self.focused_box().is_some() {
642 return self.text_input(ev);
643 }
644 match key {
645 keys::ESCAPE => {
646 if self.with_cx(|vm, _, cx| vm.on_back(cx)) {
647 return true;
648 }
649 if self.live && screen::go_back() {
650 self.log.push("back".into());
651 return true;
652 }
653 false
654 }
655 keys::ENTER | keys::KP_ENTER | keys::SPACE => {
656 let Some(p) = self.focus.clone() else {
657 return false;
658 };
659 if self.content().get_path(&p).is_some_and(|w| w.is_interactive()) {
660 self.activate(&p);
661 return true;
662 }
663 false
664 }
665 keys::UP | keys::DOWN | keys::PAGE_UP | keys::PAGE_DOWN | keys::HOME | keys::END => {
666 self.list_key(key)
667 }
668 _ => false,
669 }
670 }
671
672 fn list_key(&mut self, key: i32) -> bool {
674 let list = self
675 .focused_list()
676 .or_else(|| self.parts.lists.iter().find(|l| l.scroller.is_some()));
677 let Some(list) = list.cloned() else {
678 return false;
679 };
680 if list.len == 0 {
681 return true;
682 }
683 let last = list.len - 1;
684 let page = match (list.item_height, list.scroller) {
685 (Some(ih), Some(s)) => {
686 let view_h = self
687 .parts
688 .scrollers
689 .get(s as usize)
690 .and_then(|s| self.node(&s.path))
691 .map_or(0.0, |n| n.rect.h);
692 ((view_h / ih.max(1.0)) as u32).max(1)
693 }
694 _ => 5,
695 };
696 let new = match (key, list.selected) {
697 (keys::HOME, _) => 0,
698 (keys::END, _) => last,
699 (_, None) => 0,
700 (keys::UP, Some(i)) => i.saturating_sub(1),
701 (keys::DOWN, Some(i)) => (i + 1).min(last),
702 (keys::PAGE_UP, Some(i)) => i.saturating_sub(page),
703 (_, Some(i)) => (i + page).min(last),
704 };
705 if list.selected != Some(new) {
706 self.dispatch(ViewEvent::Select {
707 list: list.owner,
708 index: new,
709 });
710 }
711 self.reveal = Some((list.owner, new));
712 if self.focus.is_none() {
713 self.focus = Some(list.path.clone());
714 }
715 true
716 }
717
718 fn set_focus(&mut self, path: Option<Vec<u16>>, visible: bool) {
719 for t in &mut self.parts.text_boxes {
720 t.state.blur();
721 }
722 if let Some(p) = &path {
723 if let Some(t) = self.parts.text_boxes.iter_mut().find(|t| &t.path == p) {
724 t.state.focus();
725 }
726 }
727 self.focus = path;
728 self.focus_visible = visible;
729 }
730
731 fn cycle_focus(&mut self, back: bool) {
733 let n = self.parts.focusables.len();
734 if n == 0 {
735 return;
736 }
737 let cur = self
738 .focus
739 .as_ref()
740 .and_then(|f| self.parts.focusables.iter().position(|p| p == f));
741 for step in 1..=n {
742 let i = match (cur, back) {
743 (None, false) => step - 1,
744 (None, true) => n - step,
745 (Some(c), false) => (c + step) % n,
746 (Some(c), true) => (c + n - step % n) % n,
747 };
748 let p = &self.parts.focusables[i];
749 if self.node(p).is_some_and(|n| n.visible && n.enabled && n.rect.w > 0.0) {
750 let p = p.clone();
751 self.set_focus(Some(p), true);
752 return;
753 }
754 }
755 }
756}