use std::cell::{Cell, RefCell};
use std::rc::Rc;
use crate::signal::{ObserverHandle, Signal};
use super::command::{UserAttentionKind, WindowCommand};
use super::id::TeksiloWindowId;
use super::placement::WindowPlacement;
#[derive(Clone)]
pub struct WindowState {
inner: Rc<WindowStateInner>,
}
impl std::fmt::Debug for WindowState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("WindowState")
.field("id", &self.inner.id)
.field("string_id", &self.inner.string_id)
.field("placement", &self.inner.placement.get())
.field("title", &self.inner.title.get())
.field("size", &self.inner.size.get())
.field("position", &self.inner.position.get())
.field("focused", &self.inner.focused.get())
.field("resizable", &self.inner.resizable.get())
.field("always_on_top", &self.inner.always_on_top.get())
.field(
"pending_commands",
&self.inner.pending_os_commands.borrow().len(),
)
.finish()
}
}
#[derive(Debug, Clone)]
pub struct WindowStateInit {
pub id: TeksiloWindowId,
pub string_id: Option<String>,
pub placement: WindowPlacement,
pub title: String,
pub size: (u32, u32),
pub position: (i32, i32),
pub focused: bool,
pub resizable: bool,
pub always_on_top: bool,
}
pub(crate) struct WindowStateInner {
id: TeksiloWindowId,
string_id: Option<String>,
placement: Signal<WindowPlacement>,
title: Signal<String>,
size: Signal<(u32, u32)>,
position: Signal<(i32, i32)>,
focused: Signal<bool>,
resizable: Signal<bool>,
always_on_top: Signal<bool>,
caps_lock: Signal<bool>,
alt_down: Signal<bool>,
other_key_pressed_during_alt: Cell<bool>,
menubar_dispatcher: Rc<super::menubar_dispatcher::MenubarDispatcherSlot>,
pending_os_commands: RefCell<Vec<WindowCommand>>,
pending_activation_token: RefCell<Option<String>>,
applying_from_os: Cell<bool>,
_observer_handles: RefCell<Vec<ObserverHandle>>,
}
impl WindowState {
pub fn new(init: WindowStateInit) -> Self {
let inner = Rc::new(WindowStateInner {
id: init.id,
string_id: init.string_id,
placement: Signal::new(init.placement),
title: Signal::new(init.title),
size: Signal::new(init.size),
position: Signal::new(init.position),
focused: Signal::new(init.focused),
resizable: Signal::new(init.resizable),
always_on_top: Signal::new(init.always_on_top),
caps_lock: Signal::new(false),
alt_down: Signal::new(false),
other_key_pressed_during_alt: Cell::new(false),
menubar_dispatcher: Rc::new(RefCell::new(None)),
pending_os_commands: RefCell::new(Vec::new()),
pending_activation_token: RefCell::new(None),
applying_from_os: Cell::new(false),
_observer_handles: RefCell::new(Vec::new()),
});
let mut handles = Vec::new();
{
let inner_w = Rc::downgrade(&inner);
handles.push(inner.placement.observe(move |v| {
if let Some(inner) = inner_w.upgrade() {
inner.enqueue_unless_from_os(WindowCommand::SetPlacement(*v));
}
}));
}
{
let inner_w = Rc::downgrade(&inner);
handles.push(inner.title.observe(move |v| {
if let Some(inner) = inner_w.upgrade() {
inner.enqueue_unless_from_os(WindowCommand::SetTitle(v.clone()));
}
}));
}
{
let inner_w = Rc::downgrade(&inner);
handles.push(inner.size.observe(move |v| {
if let Some(inner) = inner_w.upgrade() {
inner.enqueue_unless_from_os(WindowCommand::SetSize(v.0, v.1));
}
}));
}
{
let inner_w = Rc::downgrade(&inner);
handles.push(inner.position.observe(move |v| {
if let Some(inner) = inner_w.upgrade() {
inner.enqueue_unless_from_os(WindowCommand::SetPosition(v.0, v.1));
}
}));
}
{
let inner_w = Rc::downgrade(&inner);
handles.push(inner.resizable.observe(move |v| {
if let Some(inner) = inner_w.upgrade() {
inner.enqueue_unless_from_os(WindowCommand::SetResizable(*v));
}
}));
}
{
let inner_w = Rc::downgrade(&inner);
handles.push(inner.always_on_top.observe(move |v| {
if let Some(inner) = inner_w.upgrade() {
inner.enqueue_unless_from_os(WindowCommand::SetAlwaysOnTop(*v));
}
}));
}
*inner._observer_handles.borrow_mut() = handles;
Self { inner }
}
pub fn id(&self) -> TeksiloWindowId {
self.inner.id
}
pub fn string_id(&self) -> Option<&str> {
self.inner.string_id.as_deref()
}
pub fn placement(&self) -> &Signal<WindowPlacement> {
&self.inner.placement
}
pub fn title(&self) -> &Signal<String> {
&self.inner.title
}
pub fn size(&self) -> &Signal<(u32, u32)> {
&self.inner.size
}
pub fn position(&self) -> &Signal<(i32, i32)> {
&self.inner.position
}
pub fn focused(&self) -> &Signal<bool> {
&self.inner.focused
}
pub fn resizable(&self) -> &Signal<bool> {
&self.inner.resizable
}
pub fn always_on_top(&self) -> &Signal<bool> {
&self.inner.always_on_top
}
pub fn caps_lock(&self) -> &Signal<bool> {
&self.inner.caps_lock
}
pub fn alt_down(&self) -> &Signal<bool> {
&self.inner.alt_down
}
pub fn other_key_pressed_during_alt(&self) -> bool {
self.inner.other_key_pressed_during_alt.get()
}
pub fn install_menubar_dispatcher(
&self,
dispatcher: Rc<dyn super::menubar_dispatcher::MenubarDispatcher>,
) -> super::menubar_dispatcher::MenubarGuard {
let slot = self.inner.menubar_dispatcher.clone();
{
let mut slot_ref = slot.borrow_mut();
debug_assert!(
slot_ref.is_none(),
"WindowState: a menubar dispatcher is already installed for this \
window — only one keyboard-dispatching MenuBar is supported per \
window because the dispatcher slot routes F10 / Alt+letter / \
Alt-tap to exactly one trigger set.\n\n\
\
Causes typically fall into one of:\n\
\
1. Two MenuBar widgets are mounted in the same window. Pick \
the primary one — the one that should own F10 / Alt+letter \
— and call `.no_dispatcher_install()` on every secondary \
MenuBar (showcase content, embedded demos, settings \
previews, …). Secondary MenuBars still render and respond \
to mouse + arrow-key navigation; only the window-level \
keyboard dispatch is left to the primary.\n\
\
2. A MenuBar was added with `tree.add_boxed(MenuBar::new()…)` \
from inside another MenuBar's tab / popover / submenu. Same \
fix: mark the inner one with `.no_dispatcher_install()`.\n\
\
3. The host widget that owns the MenuBar reconstructs a fresh \
`MenuBar::new()` on every rebuild without dropping its \
previous `MenubarGuard` first. `MenuBar::build` already \
handles its own rebuild path; if you're hand-rolling the \
install (custom MenubarDispatcher impl), drop the old \
`MenubarGuard` BEFORE calling this method again."
);
*slot_ref = Some(dispatcher.clone());
}
super::menubar_dispatcher::MenubarGuard {
slot,
own: dispatcher,
}
}
pub fn menubar_dispatcher(
&self,
) -> Option<Rc<dyn super::menubar_dispatcher::MenubarDispatcher>> {
self.inner.menubar_dispatcher.borrow().clone()
}
pub fn request_attention(&self, kind: UserAttentionKind) {
self.inner
.pending_os_commands
.borrow_mut()
.push(WindowCommand::RequestAttention(kind));
}
pub fn focus(&self) {
let activation_token = self.inner.pending_activation_token.borrow_mut().take();
self.inner
.pending_os_commands
.borrow_mut()
.push(WindowCommand::Focus { activation_token });
}
pub fn set_activation_token(&self, token: String) {
*self.inner.pending_activation_token.borrow_mut() = Some(token);
}
pub fn close(&self) {
self.inner
.pending_os_commands
.borrow_mut()
.push(WindowCommand::Close);
}
#[cfg(test)]
pub(crate) fn pending_command_count(&self) -> usize {
self.inner.pending_os_commands.borrow().len()
}
}
impl WindowState {
pub fn drain_os_commands(&self) -> Vec<WindowCommand> {
std::mem::take(&mut *self.inner.pending_os_commands.borrow_mut())
}
pub fn set_placement_from_os(&self, p: WindowPlacement) {
self.inner.with_os_guard(|| self.inner.placement.set(p));
}
pub fn set_title_from_os(&self, title: String) {
self.inner.with_os_guard(|| self.inner.title.set(title));
}
pub fn set_size_from_os(&self, size: (u32, u32)) {
self.inner.with_os_guard(|| self.inner.size.set(size));
}
pub fn set_position_from_os(&self, position: (i32, i32)) {
self.inner
.with_os_guard(|| self.inner.position.set(position));
}
pub fn set_focused_from_os(&self, focused: bool) {
self.inner.with_os_guard(|| self.inner.focused.set(focused));
}
pub fn set_resizable_from_os(&self, resizable: bool) {
self.inner
.with_os_guard(|| self.inner.resizable.set(resizable));
}
pub fn set_always_on_top_from_os(&self, on_top: bool) {
self.inner
.with_os_guard(|| self.inner.always_on_top.set(on_top));
}
pub fn set_caps_lock_from_os(&self, active: bool) {
if self.inner.caps_lock.get() != active {
self.inner.caps_lock.set(active);
}
}
pub fn set_alt_from_os(&self, active: bool) {
if self.inner.alt_down.get() != active {
self.inner.alt_down.set(active);
if active {
self.inner.other_key_pressed_during_alt.set(false);
}
}
}
pub fn note_non_alt_keydown_during_alt(&self) {
if self.inner.alt_down.get() {
self.inner.other_key_pressed_during_alt.set(true);
}
}
}
impl WindowStateInner {
fn enqueue_unless_from_os(&self, cmd: WindowCommand) {
if self.applying_from_os.get() {
return;
}
self.pending_os_commands.borrow_mut().push(cmd);
}
fn with_os_guard<R>(&self, f: impl FnOnce() -> R) -> R {
let prev = self.applying_from_os.replace(true);
let out = f();
self.applying_from_os.set(prev);
out
}
}
#[cfg(test)]
mod tests {
use super::*;
fn init(id: u64) -> WindowStateInit {
WindowStateInit {
id: TeksiloWindowId::new(id),
string_id: Some("test".to_string()),
placement: WindowPlacement::Floating,
title: "Test".to_string(),
size: (800, 600),
position: (0, 0),
focused: false,
resizable: true,
always_on_top: false,
}
}
#[test]
fn app_side_write_enqueues_command() {
let state = WindowState::new(init(1));
state.placement().set(WindowPlacement::Fullscreen);
let cmds = state.drain_os_commands();
assert_eq!(
cmds,
vec![WindowCommand::SetPlacement(WindowPlacement::Fullscreen)]
);
}
#[test]
fn os_side_write_does_not_enqueue_command() {
let state = WindowState::new(init(1));
state.set_placement_from_os(WindowPlacement::Maximized);
assert_eq!(state.placement().get(), WindowPlacement::Maximized);
assert_eq!(state.drain_os_commands(), vec![]);
}
#[test]
fn os_side_write_still_notifies_derived_signals() {
let state = WindowState::new(init(1));
let is_fs = state.placement().map(|p| p.is_fullscreen());
assert!(!is_fs.get());
state.set_placement_from_os(WindowPlacement::Fullscreen);
assert!(is_fs.get());
assert_eq!(state.drain_os_commands(), vec![]);
}
#[test]
fn drain_is_consuming() {
let state = WindowState::new(init(1));
state.title().set("One".to_string());
state.title().set("Two".to_string());
assert_eq!(state.pending_command_count(), 2);
let _ = state.drain_os_commands();
assert_eq!(state.pending_command_count(), 0);
}
#[test]
fn focus_close_attention_do_not_depend_on_signals() {
let state = WindowState::new(init(1));
state.focus();
state.close();
state.request_attention(UserAttentionKind::Critical);
let cmds = state.drain_os_commands();
assert_eq!(
cmds,
vec![
WindowCommand::Focus {
activation_token: None
},
WindowCommand::Close,
WindowCommand::RequestAttention(UserAttentionKind::Critical),
]
);
}
#[test]
fn multiple_app_writes_of_different_fields() {
let state = WindowState::new(init(1));
state.title().set("Hello".to_string());
state.size().set((1200, 800));
state.resizable().set(false);
let cmds = state.drain_os_commands();
assert_eq!(
cmds,
vec![
WindowCommand::SetTitle("Hello".to_string()),
WindowCommand::SetSize(1200, 800),
WindowCommand::SetResizable(false),
]
);
}
#[test]
fn guard_is_scoped_to_a_single_from_os_call() {
let state = WindowState::new(init(1));
state.set_size_from_os((1024, 768));
assert_eq!(state.drain_os_commands(), vec![]);
state.size().set((640, 480));
assert_eq!(
state.drain_os_commands(),
vec![WindowCommand::SetSize(640, 480)]
);
}
#[test]
fn id_and_string_id_accessors() {
let state = WindowState::new(init(42));
assert_eq!(state.id(), TeksiloWindowId::new(42));
assert_eq!(state.string_id(), Some("test"));
}
#[test]
fn state_is_cloneable_and_shares_storage() {
let a = WindowState::new(init(1));
let b = a.clone();
a.title().set("From a".to_string());
assert_eq!(b.title().get(), "From a");
let cmds = b.drain_os_commands();
assert_eq!(cmds.len(), 1);
assert_eq!(a.pending_command_count(), 0);
}
#[test]
fn alt_down_signal_defaults_false() {
let state = WindowState::new(init(1));
assert!(!state.alt_down().get());
assert!(!state.other_key_pressed_during_alt());
}
#[test]
fn set_alt_from_os_toggles_signal() {
let state = WindowState::new(init(1));
state.set_alt_from_os(true);
assert!(state.alt_down().get());
state.set_alt_from_os(false);
assert!(!state.alt_down().get());
}
#[test]
fn set_alt_from_os_does_not_enqueue_command() {
let state = WindowState::new(init(1));
state.set_alt_from_os(true);
assert!(state.drain_os_commands().is_empty());
}
#[test]
fn alt_down_edge_resets_other_key_flag() {
let state = WindowState::new(init(1));
state.set_alt_from_os(true);
state.note_non_alt_keydown_during_alt();
assert!(state.other_key_pressed_during_alt());
state.set_alt_from_os(false);
assert!(state.other_key_pressed_during_alt());
state.set_alt_from_os(true);
assert!(!state.other_key_pressed_during_alt());
}
#[test]
fn note_non_alt_keydown_is_noop_when_alt_not_held() {
let state = WindowState::new(init(1));
state.note_non_alt_keydown_during_alt();
assert!(!state.other_key_pressed_during_alt());
}
#[test]
fn alt_signal_observers_fire_on_transition() {
let state = WindowState::new(init(1));
let received: Rc<RefCell<Vec<bool>>> = Rc::new(RefCell::new(Vec::new()));
let received_w = Rc::downgrade(&received);
let _handle = state.alt_down().observe(move |v| {
if let Some(r) = received_w.upgrade() {
r.borrow_mut().push(*v);
}
});
state.set_alt_from_os(true);
state.set_alt_from_os(true); state.set_alt_from_os(false);
assert_eq!(*received.borrow(), vec![true, false]);
}
}