1use std::cell::{Cell, RefCell};
35use std::rc::Rc;
36
37use crate::signal::{ObserverHandle, Signal};
38
39use super::command::{UserAttentionKind, WindowCommand};
40use super::id::TeksiloWindowId;
41use super::placement::WindowPlacement;
42
43#[derive(Clone)]
49pub struct WindowState {
50 inner: Rc<WindowStateInner>,
51}
52
53impl std::fmt::Debug for WindowState {
54 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55 f.debug_struct("WindowState")
56 .field("id", &self.inner.id)
57 .field("string_id", &self.inner.string_id)
58 .field("placement", &self.inner.placement.get())
59 .field("title", &self.inner.title.get())
60 .field("size", &self.inner.size.get())
61 .field("position", &self.inner.position.get())
62 .field("focused", &self.inner.focused.get())
63 .field("resizable", &self.inner.resizable.get())
64 .field("always_on_top", &self.inner.always_on_top.get())
65 .field(
66 "pending_commands",
67 &self.inner.pending_os_commands.borrow().len(),
68 )
69 .finish()
70 }
71}
72
73#[derive(Debug, Clone)]
78pub struct WindowStateInit {
79 pub id: TeksiloWindowId,
80 pub string_id: Option<String>,
81 pub placement: WindowPlacement,
82 pub title: String,
83 pub size: (u32, u32),
84 pub position: (i32, i32),
85 pub focused: bool,
86 pub resizable: bool,
87 pub always_on_top: bool,
88}
89
90pub(crate) struct WindowStateInner {
91 id: TeksiloWindowId,
92 string_id: Option<String>,
93
94 placement: Signal<WindowPlacement>,
95 title: Signal<String>,
96 size: Signal<(u32, u32)>,
97 position: Signal<(i32, i32)>,
98 focused: Signal<bool>,
99 resizable: Signal<bool>,
100 always_on_top: Signal<bool>,
101 caption_buttons: Cell<(bool, bool)>,
105 caps_lock: Signal<bool>,
110
111 alt_down: Signal<bool>,
125
126 other_key_pressed_during_alt: Cell<bool>,
139
140 alt_press_seen: Cell<bool>,
148
149 menubar_dispatcher: Rc<super::menubar_dispatcher::MenubarDispatcherSlot>,
155
156 pending_os_commands: RefCell<Vec<WindowCommand>>,
159
160 pending_activation_token: RefCell<Option<String>>,
166
167 applying_from_os: Cell<bool>,
172
173 _observer_handles: RefCell<Vec<ObserverHandle>>,
178}
179
180impl WindowState {
181 pub fn new(init: WindowStateInit) -> Self {
187 let inner = Rc::new(WindowStateInner {
188 id: init.id,
189 string_id: init.string_id,
190 placement: Signal::new(init.placement),
191 title: Signal::new(init.title),
192 size: Signal::new(init.size),
193 position: Signal::new(init.position),
194 focused: Signal::new(init.focused),
195 resizable: Signal::new(init.resizable),
196 always_on_top: Signal::new(init.always_on_top),
197 caption_buttons: Cell::new((true, true)),
198 caps_lock: Signal::new(false),
199 alt_down: Signal::new(false),
200 other_key_pressed_during_alt: Cell::new(false),
201 alt_press_seen: Cell::new(false),
202 menubar_dispatcher: Rc::new(RefCell::new(None)),
203 pending_os_commands: RefCell::new(Vec::new()),
204 pending_activation_token: RefCell::new(None),
205 applying_from_os: Cell::new(false),
206 _observer_handles: RefCell::new(Vec::new()),
207 });
208
209 let mut handles = Vec::new();
212
213 {
214 let inner_w = Rc::downgrade(&inner);
215 handles.push(inner.placement.observe(move |v| {
216 if let Some(inner) = inner_w.upgrade() {
217 inner.enqueue_unless_from_os(WindowCommand::SetPlacement(*v));
218 }
219 }));
220 }
221 {
222 let inner_w = Rc::downgrade(&inner);
223 handles.push(inner.title.observe(move |v| {
224 if let Some(inner) = inner_w.upgrade() {
225 inner.enqueue_unless_from_os(WindowCommand::SetTitle(v.clone()));
226 }
227 }));
228 }
229 {
230 let inner_w = Rc::downgrade(&inner);
231 handles.push(inner.size.observe(move |v| {
232 if let Some(inner) = inner_w.upgrade() {
233 inner.enqueue_unless_from_os(WindowCommand::SetSize(v.0, v.1));
234 }
235 }));
236 }
237 {
238 let inner_w = Rc::downgrade(&inner);
239 handles.push(inner.position.observe(move |v| {
240 if let Some(inner) = inner_w.upgrade() {
241 inner.enqueue_unless_from_os(WindowCommand::SetPosition(v.0, v.1));
242 }
243 }));
244 }
245 {
246 let inner_w = Rc::downgrade(&inner);
247 handles.push(inner.resizable.observe(move |v| {
248 if let Some(inner) = inner_w.upgrade() {
249 inner.enqueue_unless_from_os(WindowCommand::SetResizable(*v));
250 }
251 }));
252 }
253 {
254 let inner_w = Rc::downgrade(&inner);
255 handles.push(inner.always_on_top.observe(move |v| {
256 if let Some(inner) = inner_w.upgrade() {
257 inner.enqueue_unless_from_os(WindowCommand::SetAlwaysOnTop(*v));
258 }
259 }));
260 }
261 *inner._observer_handles.borrow_mut() = handles;
267 Self { inner }
268 }
269
270 pub fn id(&self) -> TeksiloWindowId {
271 self.inner.id
272 }
273
274 pub fn string_id(&self) -> Option<&str> {
275 self.inner.string_id.as_deref()
276 }
277
278 pub fn placement(&self) -> &Signal<WindowPlacement> {
279 &self.inner.placement
280 }
281
282 pub fn title(&self) -> &Signal<String> {
283 &self.inner.title
284 }
285
286 pub fn size(&self) -> &Signal<(u32, u32)> {
287 &self.inner.size
288 }
289
290 pub fn position(&self) -> &Signal<(i32, i32)> {
291 &self.inner.position
292 }
293
294 pub fn focused(&self) -> &Signal<bool> {
295 &self.inner.focused
296 }
297
298 pub fn resizable(&self) -> &Signal<bool> {
299 &self.inner.resizable
300 }
301
302 pub fn always_on_top(&self) -> &Signal<bool> {
303 &self.inner.always_on_top
304 }
305
306 #[doc(hidden)]
311 pub fn is_minimizable(&self) -> bool {
312 self.inner.caption_buttons.get().0
313 }
314
315 #[doc(hidden)]
318 pub fn is_maximizable(&self) -> bool {
319 self.inner.caption_buttons.get().1
320 }
321
322 #[doc(hidden)]
326 pub fn set_caption_buttons(&self, minimizable: bool, maximizable: bool) {
327 self.inner.caption_buttons.set((minimizable, maximizable));
328 }
329
330 pub fn caps_lock(&self) -> &Signal<bool> {
334 &self.inner.caps_lock
335 }
336
337 pub fn alt_down(&self) -> &Signal<bool> {
344 &self.inner.alt_down
345 }
346
347 pub fn other_key_pressed_during_alt(&self) -> bool {
353 self.inner.other_key_pressed_during_alt.get()
354 }
355
356 pub fn install_menubar_dispatcher(
366 &self,
367 dispatcher: Rc<dyn super::menubar_dispatcher::MenubarDispatcher>,
368 ) -> super::menubar_dispatcher::MenubarGuard {
369 let slot = self.inner.menubar_dispatcher.clone();
370 {
371 let mut slot_ref = slot.borrow_mut();
372 debug_assert!(
373 slot_ref.is_none(),
374 "WindowState: a menubar dispatcher is already installed for this \
375 window — only one keyboard-dispatching MenuBar is supported per \
376 window because the dispatcher slot routes F10 / Alt+letter / \
377 Alt-tap to exactly one trigger set.\n\n\
378 \
379 Causes typically fall into one of:\n\
380 \
381 1. Two MenuBar widgets are mounted in the same window. Pick \
382 the primary one — the one that should own F10 / Alt+letter \
383 — and call `.no_dispatcher_install()` on every secondary \
384 MenuBar (showcase content, embedded demos, settings \
385 previews, …). Secondary MenuBars still render and respond \
386 to mouse + arrow-key navigation; only the window-level \
387 keyboard dispatch is left to the primary.\n\
388 \
389 2. A MenuBar was added with `tree.add_boxed(MenuBar::new()…)` \
390 from inside another MenuBar's tab / popover / submenu. Same \
391 fix: mark the inner one with `.no_dispatcher_install()`.\n\
392 \
393 3. The host widget that owns the MenuBar reconstructs a fresh \
394 `MenuBar::new()` on every rebuild without dropping its \
395 previous `MenubarGuard` first. `MenuBar::build` already \
396 handles its own rebuild path; if you're hand-rolling the \
397 install (custom MenubarDispatcher impl), drop the old \
398 `MenubarGuard` BEFORE calling this method again."
399 );
400 *slot_ref = Some(dispatcher.clone());
401 }
402 super::menubar_dispatcher::MenubarGuard {
403 slot,
404 own: dispatcher,
405 }
406 }
407
408 pub fn menubar_dispatcher(
413 &self,
414 ) -> Option<Rc<dyn super::menubar_dispatcher::MenubarDispatcher>> {
415 self.inner.menubar_dispatcher.borrow().clone()
416 }
417
418 pub fn request_attention(&self, kind: UserAttentionKind) {
422 self.inner
423 .pending_os_commands
424 .borrow_mut()
425 .push(WindowCommand::RequestAttention(kind));
426 }
427
428 pub fn focus(&self) {
433 let activation_token = self.inner.pending_activation_token.borrow_mut().take();
434 self.inner
435 .pending_os_commands
436 .borrow_mut()
437 .push(WindowCommand::Focus { activation_token });
438 }
439
440 pub fn set_activation_token(&self, token: String) {
445 *self.inner.pending_activation_token.borrow_mut() = Some(token);
446 }
447
448 pub fn close(&self) {
451 self.inner
452 .pending_os_commands
453 .borrow_mut()
454 .push(WindowCommand::Close);
455 }
456
457 #[cfg(test)]
461 pub(crate) fn pending_command_count(&self) -> usize {
462 self.inner.pending_os_commands.borrow().len()
463 }
464}
465
466impl WindowState {
481 pub fn drain_os_commands(&self) -> Vec<WindowCommand> {
483 std::mem::take(&mut *self.inner.pending_os_commands.borrow_mut())
484 }
485
486 pub fn set_placement_from_os(&self, p: WindowPlacement) {
489 self.inner.with_os_guard(|| self.inner.placement.set(p));
490 }
491
492 pub fn set_title_from_os(&self, title: String) {
493 self.inner.with_os_guard(|| self.inner.title.set(title));
494 }
495
496 pub fn set_size_from_os(&self, size: (u32, u32)) {
497 self.inner.with_os_guard(|| self.inner.size.set(size));
498 }
499
500 pub fn set_position_from_os(&self, position: (i32, i32)) {
501 self.inner
502 .with_os_guard(|| self.inner.position.set(position));
503 }
504
505 pub fn set_focused_from_os(&self, focused: bool) {
506 if self.inner.focused.get() != focused {
507 self.inner.alt_press_seen.set(false);
509 }
510 self.inner.with_os_guard(|| self.inner.focused.set(focused));
511 }
512
513 pub fn set_resizable_from_os(&self, resizable: bool) {
514 self.inner
515 .with_os_guard(|| self.inner.resizable.set(resizable));
516 }
517
518 pub fn set_always_on_top_from_os(&self, on_top: bool) {
519 self.inner
520 .with_os_guard(|| self.inner.always_on_top.set(on_top));
521 }
522
523 pub fn set_caps_lock_from_os(&self, active: bool) {
528 if self.inner.caps_lock.get() != active {
529 self.inner.caps_lock.set(active);
530 }
531 }
532
533 pub fn set_alt_from_os(&self, active: bool) {
537 if self.inner.alt_down.get() != active {
538 self.inner.alt_down.set(active);
539 if active {
540 self.inner.other_key_pressed_during_alt.set(false);
543 }
544 }
545 }
546
547 pub fn note_non_alt_keydown_during_alt(&self) {
552 if self.inner.alt_down.get() {
553 self.inner.other_key_pressed_during_alt.set(true);
554 }
555 }
556
557 pub fn note_close_request_from_os(&self) {
563 self.note_non_alt_keydown_during_alt();
564 }
565
566 pub fn note_alt_key_from_os(&self, pressed: bool) -> bool {
572 if pressed {
573 self.inner.alt_press_seen.set(true);
574 return false;
575 }
576 self.inner.alt_press_seen.replace(false) && !self.inner.other_key_pressed_during_alt.get()
577 }
578}
579
580impl WindowStateInner {
581 fn enqueue_unless_from_os(&self, cmd: WindowCommand) {
582 if self.applying_from_os.get() {
583 return;
584 }
585 self.pending_os_commands.borrow_mut().push(cmd);
586 }
587
588 fn with_os_guard<R>(&self, f: impl FnOnce() -> R) -> R {
589 let prev = self.applying_from_os.replace(true);
592 let out = f();
593 self.applying_from_os.set(prev);
594 out
595 }
596}
597
598#[cfg(test)]
599mod tests {
600 use super::*;
601
602 fn init(id: u64) -> WindowStateInit {
603 WindowStateInit {
604 id: TeksiloWindowId::new(id),
605 string_id: Some("test".to_string()),
606 placement: WindowPlacement::Floating,
607 title: "Test".to_string(),
608 size: (800, 600),
609 position: (0, 0),
610 focused: false,
611 resizable: true,
612 always_on_top: false,
613 }
614 }
615
616 #[test]
617 fn app_side_write_enqueues_command() {
618 let state = WindowState::new(init(1));
619 state.placement().set(WindowPlacement::Fullscreen);
620 let cmds = state.drain_os_commands();
621 assert_eq!(
622 cmds,
623 vec![WindowCommand::SetPlacement(WindowPlacement::Fullscreen)]
624 );
625 }
626
627 #[test]
628 fn os_side_write_does_not_enqueue_command() {
629 let state = WindowState::new(init(1));
630 state.set_placement_from_os(WindowPlacement::Maximized);
631 assert_eq!(state.placement().get(), WindowPlacement::Maximized);
632 assert_eq!(state.drain_os_commands(), vec![]);
633 }
634
635 #[test]
636 fn os_side_write_still_notifies_derived_signals() {
637 let state = WindowState::new(init(1));
638 let is_fs = state.placement().map(|p| p.is_fullscreen());
639 assert!(!is_fs.get());
640 state.set_placement_from_os(WindowPlacement::Fullscreen);
641 assert!(is_fs.get());
642 assert_eq!(state.drain_os_commands(), vec![]);
644 }
645
646 #[test]
647 fn drain_is_consuming() {
648 let state = WindowState::new(init(1));
649 state.title().set("One".to_string());
650 state.title().set("Two".to_string());
651 assert_eq!(state.pending_command_count(), 2);
652 let _ = state.drain_os_commands();
653 assert_eq!(state.pending_command_count(), 0);
654 }
655
656 #[test]
657 fn focus_close_attention_do_not_depend_on_signals() {
658 let state = WindowState::new(init(1));
659 state.focus();
660 state.close();
661 state.request_attention(UserAttentionKind::Critical);
662 let cmds = state.drain_os_commands();
663 assert_eq!(
664 cmds,
665 vec![
666 WindowCommand::Focus {
667 activation_token: None
668 },
669 WindowCommand::Close,
670 WindowCommand::RequestAttention(UserAttentionKind::Critical),
671 ]
672 );
673 }
674
675 #[test]
676 fn multiple_app_writes_of_different_fields() {
677 let state = WindowState::new(init(1));
678 state.title().set("Hello".to_string());
679 state.size().set((1200, 800));
680 state.resizable().set(false);
681 let cmds = state.drain_os_commands();
682 assert_eq!(
683 cmds,
684 vec![
685 WindowCommand::SetTitle("Hello".to_string()),
686 WindowCommand::SetSize(1200, 800),
687 WindowCommand::SetResizable(false),
688 ]
689 );
690 }
691
692 #[test]
693 fn guard_is_scoped_to_a_single_from_os_call() {
694 let state = WindowState::new(init(1));
695 state.set_size_from_os((1024, 768));
697 assert_eq!(state.drain_os_commands(), vec![]);
698 state.size().set((640, 480));
700 assert_eq!(
701 state.drain_os_commands(),
702 vec![WindowCommand::SetSize(640, 480)]
703 );
704 }
705
706 #[test]
707 fn id_and_string_id_accessors() {
708 let state = WindowState::new(init(42));
709 assert_eq!(state.id(), TeksiloWindowId::new(42));
710 assert_eq!(state.string_id(), Some("test"));
711 }
712
713 #[test]
714 fn state_is_cloneable_and_shares_storage() {
715 let a = WindowState::new(init(1));
716 let b = a.clone();
717 a.title().set("From a".to_string());
718 assert_eq!(b.title().get(), "From a");
719 let cmds = b.drain_os_commands();
721 assert_eq!(cmds.len(), 1);
722 assert_eq!(a.pending_command_count(), 0);
723 }
724
725 #[test]
728 fn alt_down_signal_defaults_false() {
729 let state = WindowState::new(init(1));
730 assert!(!state.alt_down().get());
731 assert!(!state.other_key_pressed_during_alt());
732 }
733
734 #[test]
735 fn set_alt_from_os_toggles_signal() {
736 let state = WindowState::new(init(1));
737 state.set_alt_from_os(true);
738 assert!(state.alt_down().get());
739 state.set_alt_from_os(false);
740 assert!(!state.alt_down().get());
741 }
742
743 #[test]
744 fn set_alt_from_os_does_not_enqueue_command() {
745 let state = WindowState::new(init(1));
747 state.set_alt_from_os(true);
748 assert!(state.drain_os_commands().is_empty());
749 }
750
751 #[test]
752 fn alt_down_edge_resets_other_key_flag() {
753 let state = WindowState::new(init(1));
754 state.set_alt_from_os(true);
756 state.note_non_alt_keydown_during_alt();
757 assert!(state.other_key_pressed_during_alt());
758 state.set_alt_from_os(false);
761 assert!(state.other_key_pressed_during_alt());
762 state.set_alt_from_os(true);
765 assert!(!state.other_key_pressed_during_alt());
766 }
767
768 #[test]
769 fn note_non_alt_keydown_is_noop_when_alt_not_held() {
770 let state = WindowState::new(init(1));
771 state.note_non_alt_keydown_during_alt();
772 assert!(!state.other_key_pressed_during_alt());
773 }
774
775 #[test]
778 fn a_press_and_release_seen_here_with_nothing_between_is_a_tap() {
779 let state = WindowState::new(init(1));
780 state.set_focused_from_os(true);
781 state.set_alt_from_os(true);
783 assert!(!state.note_alt_key_from_os(true));
784 state.set_alt_from_os(false);
785 assert!(state.note_alt_key_from_os(false));
786 assert!(!state.note_alt_key_from_os(true));
788 state.set_alt_from_os(true);
789 assert!(state.note_alt_key_from_os(false));
790 state.set_alt_from_os(false);
791 }
792
793 #[test]
794 fn a_chord_is_not_a_tap() {
795 let state = WindowState::new(init(1));
796 state.set_focused_from_os(true);
797 state.set_alt_from_os(true);
798 state.note_alt_key_from_os(true);
799 state.note_non_alt_keydown_during_alt();
800 state.set_alt_from_os(false);
801 assert!(!state.note_alt_key_from_os(false));
802 }
803
804 #[test]
808 fn a_hold_begun_in_another_window_is_not_a_tap() {
809 let state = WindowState::new(init(1));
810 state.set_focused_from_os(false);
811 state.set_focused_from_os(true);
812 state.set_alt_from_os(true);
813 state.set_alt_from_os(false);
814 assert!(!state.note_alt_key_from_os(false));
815 }
816
817 #[test]
820 fn alt_f4_is_not_a_tap() {
821 let state = WindowState::new(init(1));
822 state.set_focused_from_os(true);
823 state.set_alt_from_os(true);
824 state.note_alt_key_from_os(true);
825 state.note_close_request_from_os();
826 state.set_alt_from_os(false);
827 assert!(!state.note_alt_key_from_os(false));
828 }
829
830 #[test]
833 fn a_hold_the_focus_left_during_is_not_a_tap() {
834 let state = WindowState::new(init(1));
835 state.set_focused_from_os(true);
836 state.set_alt_from_os(true);
837 state.note_alt_key_from_os(true);
838 state.set_focused_from_os(false);
839 state.set_focused_from_os(true);
840 state.set_alt_from_os(false);
841 assert!(!state.note_alt_key_from_os(false));
842 }
843
844 #[test]
845 fn alt_signal_observers_fire_on_transition() {
846 let state = WindowState::new(init(1));
847 let received: Rc<RefCell<Vec<bool>>> = Rc::new(RefCell::new(Vec::new()));
848 let received_w = Rc::downgrade(&received);
849 let _handle = state.alt_down().observe(move |v| {
850 if let Some(r) = received_w.upgrade() {
851 r.borrow_mut().push(*v);
852 }
853 });
854 state.set_alt_from_os(true);
855 state.set_alt_from_os(true); state.set_alt_from_os(false);
857 assert_eq!(*received.borrow(), vec![true, false]);
858 }
859}