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>,
122
123 other_key_pressed_during_alt: Cell<bool>,
136
137 menubar_dispatcher: Rc<super::menubar_dispatcher::MenubarDispatcherSlot>,
143
144 pending_os_commands: RefCell<Vec<WindowCommand>>,
147
148 pending_activation_token: RefCell<Option<String>>,
154
155 applying_from_os: Cell<bool>,
160
161 _observer_handles: RefCell<Vec<ObserverHandle>>,
166}
167
168impl WindowState {
169 pub fn new(init: WindowStateInit) -> Self {
175 let inner = Rc::new(WindowStateInner {
176 id: init.id,
177 string_id: init.string_id,
178 placement: Signal::new(init.placement),
179 title: Signal::new(init.title),
180 size: Signal::new(init.size),
181 position: Signal::new(init.position),
182 focused: Signal::new(init.focused),
183 resizable: Signal::new(init.resizable),
184 always_on_top: Signal::new(init.always_on_top),
185 caption_buttons: Cell::new((true, true)),
186 caps_lock: Signal::new(false),
187 alt_down: Signal::new(false),
188 other_key_pressed_during_alt: Cell::new(false),
189 menubar_dispatcher: Rc::new(RefCell::new(None)),
190 pending_os_commands: RefCell::new(Vec::new()),
191 pending_activation_token: RefCell::new(None),
192 applying_from_os: Cell::new(false),
193 _observer_handles: RefCell::new(Vec::new()),
194 });
195
196 let mut handles = Vec::new();
199
200 {
201 let inner_w = Rc::downgrade(&inner);
202 handles.push(inner.placement.observe(move |v| {
203 if let Some(inner) = inner_w.upgrade() {
204 inner.enqueue_unless_from_os(WindowCommand::SetPlacement(*v));
205 }
206 }));
207 }
208 {
209 let inner_w = Rc::downgrade(&inner);
210 handles.push(inner.title.observe(move |v| {
211 if let Some(inner) = inner_w.upgrade() {
212 inner.enqueue_unless_from_os(WindowCommand::SetTitle(v.clone()));
213 }
214 }));
215 }
216 {
217 let inner_w = Rc::downgrade(&inner);
218 handles.push(inner.size.observe(move |v| {
219 if let Some(inner) = inner_w.upgrade() {
220 inner.enqueue_unless_from_os(WindowCommand::SetSize(v.0, v.1));
221 }
222 }));
223 }
224 {
225 let inner_w = Rc::downgrade(&inner);
226 handles.push(inner.position.observe(move |v| {
227 if let Some(inner) = inner_w.upgrade() {
228 inner.enqueue_unless_from_os(WindowCommand::SetPosition(v.0, v.1));
229 }
230 }));
231 }
232 {
233 let inner_w = Rc::downgrade(&inner);
234 handles.push(inner.resizable.observe(move |v| {
235 if let Some(inner) = inner_w.upgrade() {
236 inner.enqueue_unless_from_os(WindowCommand::SetResizable(*v));
237 }
238 }));
239 }
240 {
241 let inner_w = Rc::downgrade(&inner);
242 handles.push(inner.always_on_top.observe(move |v| {
243 if let Some(inner) = inner_w.upgrade() {
244 inner.enqueue_unless_from_os(WindowCommand::SetAlwaysOnTop(*v));
245 }
246 }));
247 }
248 *inner._observer_handles.borrow_mut() = handles;
254 Self { inner }
255 }
256
257 pub fn id(&self) -> TeksiloWindowId {
258 self.inner.id
259 }
260
261 pub fn string_id(&self) -> Option<&str> {
262 self.inner.string_id.as_deref()
263 }
264
265 pub fn placement(&self) -> &Signal<WindowPlacement> {
266 &self.inner.placement
267 }
268
269 pub fn title(&self) -> &Signal<String> {
270 &self.inner.title
271 }
272
273 pub fn size(&self) -> &Signal<(u32, u32)> {
274 &self.inner.size
275 }
276
277 pub fn position(&self) -> &Signal<(i32, i32)> {
278 &self.inner.position
279 }
280
281 pub fn focused(&self) -> &Signal<bool> {
282 &self.inner.focused
283 }
284
285 pub fn resizable(&self) -> &Signal<bool> {
286 &self.inner.resizable
287 }
288
289 pub fn always_on_top(&self) -> &Signal<bool> {
290 &self.inner.always_on_top
291 }
292
293 #[doc(hidden)]
298 pub fn is_minimizable(&self) -> bool {
299 self.inner.caption_buttons.get().0
300 }
301
302 #[doc(hidden)]
305 pub fn is_maximizable(&self) -> bool {
306 self.inner.caption_buttons.get().1
307 }
308
309 #[doc(hidden)]
313 pub fn set_caption_buttons(&self, minimizable: bool, maximizable: bool) {
314 self.inner.caption_buttons.set((minimizable, maximizable));
315 }
316
317 pub fn caps_lock(&self) -> &Signal<bool> {
321 &self.inner.caps_lock
322 }
323
324 pub fn alt_down(&self) -> &Signal<bool> {
331 &self.inner.alt_down
332 }
333
334 pub fn other_key_pressed_during_alt(&self) -> bool {
339 self.inner.other_key_pressed_during_alt.get()
340 }
341
342 pub fn install_menubar_dispatcher(
352 &self,
353 dispatcher: Rc<dyn super::menubar_dispatcher::MenubarDispatcher>,
354 ) -> super::menubar_dispatcher::MenubarGuard {
355 let slot = self.inner.menubar_dispatcher.clone();
356 {
357 let mut slot_ref = slot.borrow_mut();
358 debug_assert!(
359 slot_ref.is_none(),
360 "WindowState: a menubar dispatcher is already installed for this \
361 window — only one keyboard-dispatching MenuBar is supported per \
362 window because the dispatcher slot routes F10 / Alt+letter / \
363 Alt-tap to exactly one trigger set.\n\n\
364 \
365 Causes typically fall into one of:\n\
366 \
367 1. Two MenuBar widgets are mounted in the same window. Pick \
368 the primary one — the one that should own F10 / Alt+letter \
369 — and call `.no_dispatcher_install()` on every secondary \
370 MenuBar (showcase content, embedded demos, settings \
371 previews, …). Secondary MenuBars still render and respond \
372 to mouse + arrow-key navigation; only the window-level \
373 keyboard dispatch is left to the primary.\n\
374 \
375 2. A MenuBar was added with `tree.add_boxed(MenuBar::new()…)` \
376 from inside another MenuBar's tab / popover / submenu. Same \
377 fix: mark the inner one with `.no_dispatcher_install()`.\n\
378 \
379 3. The host widget that owns the MenuBar reconstructs a fresh \
380 `MenuBar::new()` on every rebuild without dropping its \
381 previous `MenubarGuard` first. `MenuBar::build` already \
382 handles its own rebuild path; if you're hand-rolling the \
383 install (custom MenubarDispatcher impl), drop the old \
384 `MenubarGuard` BEFORE calling this method again."
385 );
386 *slot_ref = Some(dispatcher.clone());
387 }
388 super::menubar_dispatcher::MenubarGuard {
389 slot,
390 own: dispatcher,
391 }
392 }
393
394 pub fn menubar_dispatcher(
399 &self,
400 ) -> Option<Rc<dyn super::menubar_dispatcher::MenubarDispatcher>> {
401 self.inner.menubar_dispatcher.borrow().clone()
402 }
403
404 pub fn request_attention(&self, kind: UserAttentionKind) {
408 self.inner
409 .pending_os_commands
410 .borrow_mut()
411 .push(WindowCommand::RequestAttention(kind));
412 }
413
414 pub fn focus(&self) {
419 let activation_token = self.inner.pending_activation_token.borrow_mut().take();
420 self.inner
421 .pending_os_commands
422 .borrow_mut()
423 .push(WindowCommand::Focus { activation_token });
424 }
425
426 pub fn set_activation_token(&self, token: String) {
431 *self.inner.pending_activation_token.borrow_mut() = Some(token);
432 }
433
434 pub fn close(&self) {
437 self.inner
438 .pending_os_commands
439 .borrow_mut()
440 .push(WindowCommand::Close);
441 }
442
443 #[cfg(test)]
447 pub(crate) fn pending_command_count(&self) -> usize {
448 self.inner.pending_os_commands.borrow().len()
449 }
450}
451
452impl WindowState {
467 pub fn drain_os_commands(&self) -> Vec<WindowCommand> {
469 std::mem::take(&mut *self.inner.pending_os_commands.borrow_mut())
470 }
471
472 pub fn set_placement_from_os(&self, p: WindowPlacement) {
475 self.inner.with_os_guard(|| self.inner.placement.set(p));
476 }
477
478 pub fn set_title_from_os(&self, title: String) {
479 self.inner.with_os_guard(|| self.inner.title.set(title));
480 }
481
482 pub fn set_size_from_os(&self, size: (u32, u32)) {
483 self.inner.with_os_guard(|| self.inner.size.set(size));
484 }
485
486 pub fn set_position_from_os(&self, position: (i32, i32)) {
487 self.inner
488 .with_os_guard(|| self.inner.position.set(position));
489 }
490
491 pub fn set_focused_from_os(&self, focused: bool) {
492 self.inner.with_os_guard(|| self.inner.focused.set(focused));
493 }
494
495 pub fn set_resizable_from_os(&self, resizable: bool) {
496 self.inner
497 .with_os_guard(|| self.inner.resizable.set(resizable));
498 }
499
500 pub fn set_always_on_top_from_os(&self, on_top: bool) {
501 self.inner
502 .with_os_guard(|| self.inner.always_on_top.set(on_top));
503 }
504
505 pub fn set_caps_lock_from_os(&self, active: bool) {
510 if self.inner.caps_lock.get() != active {
511 self.inner.caps_lock.set(active);
512 }
513 }
514
515 pub fn set_alt_from_os(&self, active: bool) {
519 if self.inner.alt_down.get() != active {
520 self.inner.alt_down.set(active);
521 if active {
522 self.inner.other_key_pressed_during_alt.set(false);
525 }
526 }
527 }
528
529 pub fn note_non_alt_keydown_during_alt(&self) {
534 if self.inner.alt_down.get() {
535 self.inner.other_key_pressed_during_alt.set(true);
536 }
537 }
538}
539
540impl WindowStateInner {
541 fn enqueue_unless_from_os(&self, cmd: WindowCommand) {
542 if self.applying_from_os.get() {
543 return;
544 }
545 self.pending_os_commands.borrow_mut().push(cmd);
546 }
547
548 fn with_os_guard<R>(&self, f: impl FnOnce() -> R) -> R {
549 let prev = self.applying_from_os.replace(true);
552 let out = f();
553 self.applying_from_os.set(prev);
554 out
555 }
556}
557
558#[cfg(test)]
559mod tests {
560 use super::*;
561
562 fn init(id: u64) -> WindowStateInit {
563 WindowStateInit {
564 id: TeksiloWindowId::new(id),
565 string_id: Some("test".to_string()),
566 placement: WindowPlacement::Floating,
567 title: "Test".to_string(),
568 size: (800, 600),
569 position: (0, 0),
570 focused: false,
571 resizable: true,
572 always_on_top: false,
573 }
574 }
575
576 #[test]
577 fn app_side_write_enqueues_command() {
578 let state = WindowState::new(init(1));
579 state.placement().set(WindowPlacement::Fullscreen);
580 let cmds = state.drain_os_commands();
581 assert_eq!(
582 cmds,
583 vec![WindowCommand::SetPlacement(WindowPlacement::Fullscreen)]
584 );
585 }
586
587 #[test]
588 fn os_side_write_does_not_enqueue_command() {
589 let state = WindowState::new(init(1));
590 state.set_placement_from_os(WindowPlacement::Maximized);
591 assert_eq!(state.placement().get(), WindowPlacement::Maximized);
592 assert_eq!(state.drain_os_commands(), vec![]);
593 }
594
595 #[test]
596 fn os_side_write_still_notifies_derived_signals() {
597 let state = WindowState::new(init(1));
598 let is_fs = state.placement().map(|p| p.is_fullscreen());
599 assert!(!is_fs.get());
600 state.set_placement_from_os(WindowPlacement::Fullscreen);
601 assert!(is_fs.get());
602 assert_eq!(state.drain_os_commands(), vec![]);
604 }
605
606 #[test]
607 fn drain_is_consuming() {
608 let state = WindowState::new(init(1));
609 state.title().set("One".to_string());
610 state.title().set("Two".to_string());
611 assert_eq!(state.pending_command_count(), 2);
612 let _ = state.drain_os_commands();
613 assert_eq!(state.pending_command_count(), 0);
614 }
615
616 #[test]
617 fn focus_close_attention_do_not_depend_on_signals() {
618 let state = WindowState::new(init(1));
619 state.focus();
620 state.close();
621 state.request_attention(UserAttentionKind::Critical);
622 let cmds = state.drain_os_commands();
623 assert_eq!(
624 cmds,
625 vec![
626 WindowCommand::Focus {
627 activation_token: None
628 },
629 WindowCommand::Close,
630 WindowCommand::RequestAttention(UserAttentionKind::Critical),
631 ]
632 );
633 }
634
635 #[test]
636 fn multiple_app_writes_of_different_fields() {
637 let state = WindowState::new(init(1));
638 state.title().set("Hello".to_string());
639 state.size().set((1200, 800));
640 state.resizable().set(false);
641 let cmds = state.drain_os_commands();
642 assert_eq!(
643 cmds,
644 vec![
645 WindowCommand::SetTitle("Hello".to_string()),
646 WindowCommand::SetSize(1200, 800),
647 WindowCommand::SetResizable(false),
648 ]
649 );
650 }
651
652 #[test]
653 fn guard_is_scoped_to_a_single_from_os_call() {
654 let state = WindowState::new(init(1));
655 state.set_size_from_os((1024, 768));
657 assert_eq!(state.drain_os_commands(), vec![]);
658 state.size().set((640, 480));
660 assert_eq!(
661 state.drain_os_commands(),
662 vec![WindowCommand::SetSize(640, 480)]
663 );
664 }
665
666 #[test]
667 fn id_and_string_id_accessors() {
668 let state = WindowState::new(init(42));
669 assert_eq!(state.id(), TeksiloWindowId::new(42));
670 assert_eq!(state.string_id(), Some("test"));
671 }
672
673 #[test]
674 fn state_is_cloneable_and_shares_storage() {
675 let a = WindowState::new(init(1));
676 let b = a.clone();
677 a.title().set("From a".to_string());
678 assert_eq!(b.title().get(), "From a");
679 let cmds = b.drain_os_commands();
681 assert_eq!(cmds.len(), 1);
682 assert_eq!(a.pending_command_count(), 0);
683 }
684
685 #[test]
688 fn alt_down_signal_defaults_false() {
689 let state = WindowState::new(init(1));
690 assert!(!state.alt_down().get());
691 assert!(!state.other_key_pressed_during_alt());
692 }
693
694 #[test]
695 fn set_alt_from_os_toggles_signal() {
696 let state = WindowState::new(init(1));
697 state.set_alt_from_os(true);
698 assert!(state.alt_down().get());
699 state.set_alt_from_os(false);
700 assert!(!state.alt_down().get());
701 }
702
703 #[test]
704 fn set_alt_from_os_does_not_enqueue_command() {
705 let state = WindowState::new(init(1));
707 state.set_alt_from_os(true);
708 assert!(state.drain_os_commands().is_empty());
709 }
710
711 #[test]
712 fn alt_down_edge_resets_other_key_flag() {
713 let state = WindowState::new(init(1));
714 state.set_alt_from_os(true);
716 state.note_non_alt_keydown_during_alt();
717 assert!(state.other_key_pressed_during_alt());
718 state.set_alt_from_os(false);
721 assert!(state.other_key_pressed_during_alt());
722 state.set_alt_from_os(true);
725 assert!(!state.other_key_pressed_during_alt());
726 }
727
728 #[test]
729 fn note_non_alt_keydown_is_noop_when_alt_not_held() {
730 let state = WindowState::new(init(1));
731 state.note_non_alt_keydown_during_alt();
732 assert!(!state.other_key_pressed_during_alt());
733 }
734
735 #[test]
736 fn alt_signal_observers_fire_on_transition() {
737 let state = WindowState::new(init(1));
738 let received: Rc<RefCell<Vec<bool>>> = Rc::new(RefCell::new(Vec::new()));
739 let received_w = Rc::downgrade(&received);
740 let _handle = state.alt_down().observe(move |v| {
741 if let Some(r) = received_w.upgrade() {
742 r.borrow_mut().push(*v);
743 }
744 });
745 state.set_alt_from_os(true);
746 state.set_alt_from_os(true); state.set_alt_from_os(false);
748 assert_eq!(*received.borrow(), vec![true, false]);
749 }
750}