Skip to main content

graphix_package_gui/
window.rs

1use crate::{
2    types::{ImageSourceV, SizeV, ThemeV},
3    widgets::{compile, EmptyW, GuiW},
4};
5use anyhow::{Context, Result};
6use arcstr::ArcStr;
7use graphix_compiler::expr::ExprId;
8use graphix_rt::{GXExt, GXHandle, Ref, TRef};
9use iced_core::mouse;
10use netidx::publisher::Value;
11use std::sync::Arc;
12use std::time::Instant;
13
14use tokio::try_join;
15use winit::window::{Window, WindowAttributes, WindowId};
16
17/// Resolved window state — all refs compiled but no OS window yet.
18pub struct ResolvedWindow<X: GXExt> {
19    pub gx: GXHandle<X>,
20    pub title: TRef<X, String>,
21    pub size: TRef<X, SizeV>,
22    pub theme: TRef<X, ThemeV>,
23    pub icon: TRef<X, ImageSourceV>,
24    pub decoded_icon: Option<winit::window::Icon>,
25    pub content_ref: Ref<X>,
26    pub content: GuiW<X>,
27}
28
29impl<X: GXExt> ResolvedWindow<X> {
30    /// Compile a window struct value into resolved refs without creating an OS window.
31    pub async fn compile(gx: GXHandle<X>, source: Value) -> Result<Self> {
32        let [(_, content), (_, icon), (_, size), (_, theme), (_, title)] =
33            source.cast_to::<[(ArcStr, u64); 5]>().context("window flds")?;
34        let (content_ref, icon, size, theme, title) = try_join! {
35            gx.compile_ref(content),
36            gx.compile_ref(icon),
37            gx.compile_ref(size),
38            gx.compile_ref(theme),
39            gx.compile_ref(title),
40        }?;
41        let compiled_content: GuiW<X> = match content_ref.last.as_ref() {
42            None => Box::new(EmptyW),
43            Some(v) => compile(gx.clone(), v.clone()).await.context("window content")?,
44        };
45        let icon = TRef::new(icon).context("window tref icon")?;
46        let decoded_icon =
47            icon.t.as_ref().and_then(|s: &ImageSourceV| match s.decode_icon() {
48                Ok(i) => i,
49                Err(e) => {
50                    log::warn!("failed to decode window icon: {e}");
51                    None
52                }
53            });
54        Ok(Self {
55            gx,
56            title: TRef::new(title).context("window tref title")?,
57            size: TRef::new(size).context("window tref size")?,
58            theme: TRef::new(theme).context("window tref theme")?,
59            icon,
60            decoded_icon,
61            content_ref,
62            content: compiled_content,
63        })
64    }
65
66    /// Build winit WindowAttributes from the resolved title/size refs.
67    pub fn window_attrs(&self) -> WindowAttributes {
68        let title = self.title.t.as_ref().map(|t| t.as_str()).unwrap_or("Graphix");
69        let (w, h) = self
70            .size
71            .t
72            .as_ref()
73            .map(|sz| (sz.0.width, sz.0.height))
74            .unwrap_or((800.0, 600.0));
75        WindowAttributes::default()
76            .with_title(title)
77            .with_inner_size(winit::dpi::LogicalSize::new(w, h))
78            .with_window_icon(self.decoded_icon.clone())
79    }
80
81    /// Consume self and attach an OS window, producing a TrackedWindow.
82    pub fn into_tracked(
83        self,
84        window_ref: Ref<X>,
85        window: Arc<Window>,
86    ) -> TrackedWindow<X> {
87        TrackedWindow {
88            window_ref,
89            gx: self.gx,
90            window,
91            title: self.title,
92            size: self.size,
93            theme: self.theme,
94            icon: self.icon,
95            decoded_icon: self.decoded_icon,
96            content_ref: self.content_ref,
97            content: self.content,
98            cursor_position: iced_core::Point::ORIGIN,
99            last_mouse_interaction: mouse::Interaction::default(),
100            pending_events: Vec::new(),
101            needs_redraw: true,
102            last_set_size: None,
103            pending_resize: None,
104            resize_timer_armed: false,
105            last_render: Instant::now(),
106        }
107    }
108}
109
110/// Per-window state tracking.
111pub struct TrackedWindow<X: GXExt> {
112    pub window_ref: Ref<X>,
113    pub gx: GXHandle<X>,
114    pub window: Arc<Window>,
115    pub title: TRef<X, String>,
116    pub size: TRef<X, SizeV>,
117    pub theme: TRef<X, ThemeV>,
118    pub icon: TRef<X, ImageSourceV>,
119    pub decoded_icon: Option<winit::window::Icon>,
120    pub content_ref: Ref<X>,
121    pub content: GuiW<X>,
122    pub cursor_position: iced_core::Point,
123    pub last_mouse_interaction: mouse::Interaction,
124    pub pending_events: Vec<iced_core::Event>,
125    pub needs_redraw: bool,
126    pub last_set_size: Option<SizeV>,
127    pub pending_resize: Option<(u32, u32, f64)>,
128    /// True while a debounce timer is armed for this window. Set when
129    /// a `Resized` event arrives and `pending_resize` transitions
130    /// `None → Some`; cleared when the timer fires. Prevents arming
131    /// multiple timers during a continuous drag.
132    pub resize_timer_armed: bool,
133    pub last_render: Instant,
134}
135
136impl<X: GXExt> TrackedWindow<X> {
137    pub fn handle_update(
138        &mut self,
139        rt: &tokio::runtime::Handle,
140        id: ExprId,
141        v: &Value,
142    ) -> Result<()> {
143        if id == self.window_ref.id {
144            self.window_ref.last = Some(v.clone());
145            let resolved = rt
146                .block_on(ResolvedWindow::compile(self.gx.clone(), v.clone()))
147                .context("window ref recompile")?;
148            self.title = resolved.title;
149            self.size = resolved.size;
150            self.theme = resolved.theme;
151            self.icon = resolved.icon;
152            self.decoded_icon = resolved.decoded_icon;
153            self.content_ref = resolved.content_ref;
154            self.content = resolved.content;
155            if let Some(t) = self.title.t.as_ref() {
156                self.window.set_title(t);
157            }
158            if let Some(sz) = self.size.t.as_ref() {
159                let _ = self.window.request_inner_size(winit::dpi::LogicalSize::new(
160                    sz.0.width,
161                    sz.0.height,
162                ));
163            }
164            self.window.set_window_icon(self.decoded_icon.clone());
165            self.needs_redraw = true;
166            return Ok(());
167        }
168        let mut changed = false;
169        if let Some(t) = self.title.update(id, v).context("window update title")? {
170            self.window.set_title(t);
171            changed = true;
172        }
173        if let Some(sz) = self.size.update(id, v).context("window update size")? {
174            if self.last_set_size.take() != Some(*sz) {
175                let _ = self.window.request_inner_size(winit::dpi::LogicalSize::new(
176                    sz.0.width,
177                    sz.0.height,
178                ));
179            }
180            changed = true;
181        }
182        if self.theme.update(id, v).context("window update theme")?.is_some() {
183            changed = true;
184        }
185        if self.icon.update(id, v).context("window update icon")?.is_some() {
186            self.decoded_icon =
187                self.icon.t.as_ref().and_then(|s: &ImageSourceV| match s.decode_icon() {
188                    Ok(i) => i,
189                    Err(e) => {
190                        log::warn!("failed to decode window icon: {e}");
191                        None
192                    }
193                });
194            self.window.set_window_icon(self.decoded_icon.clone());
195            changed = true;
196        }
197        if id == self.content_ref.id {
198            self.content_ref.last = Some(v.clone());
199            self.content = rt
200                .block_on(compile(self.gx.clone(), v.clone()))
201                .context("window content recompile")?;
202            changed = true;
203        }
204        changed |= self.content.handle_update(rt, id, v)?;
205        self.needs_redraw |= changed;
206        Ok(())
207    }
208
209    pub fn window_id(&self) -> WindowId {
210        self.window.id()
211    }
212
213    pub fn iced_theme(&self) -> crate::theme::GraphixTheme {
214        self.theme.t.as_ref().map(|t| t.0.clone()).unwrap_or(crate::theme::GraphixTheme {
215            inner: iced_core::Theme::Dark,
216            overrides: None,
217        })
218    }
219
220    pub fn push_event(&mut self, event: iced_core::Event) {
221        self.pending_events.push(event);
222        // During a resize drag, let the render-period timer drive
223        // renders. Without this guard, every `CursorMoved` during
224        // the drag (the mouse is always moving when you're dragging
225        // the corner) sets `needs_redraw = true` and the render
226        // cadence blows past the intended ~10 Hz. Events still
227        // accumulate in `pending_events` — they're processed on
228        // the next timer-driven render.
229        if !self.resize_timer_armed {
230            self.needs_redraw = true;
231        }
232    }
233
234    pub fn cursor(&self) -> mouse::Cursor {
235        mouse::Cursor::Available(self.cursor_position)
236    }
237}