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            last_render: Instant::now(),
105        }
106    }
107}
108
109/// Per-window state tracking.
110pub struct TrackedWindow<X: GXExt> {
111    pub window_ref: Ref<X>,
112    pub gx: GXHandle<X>,
113    pub window: Arc<Window>,
114    pub title: TRef<X, String>,
115    pub size: TRef<X, SizeV>,
116    pub theme: TRef<X, ThemeV>,
117    pub icon: TRef<X, ImageSourceV>,
118    pub decoded_icon: Option<winit::window::Icon>,
119    pub content_ref: Ref<X>,
120    pub content: GuiW<X>,
121    pub cursor_position: iced_core::Point,
122    pub last_mouse_interaction: mouse::Interaction,
123    pub pending_events: Vec<iced_core::Event>,
124    pub needs_redraw: bool,
125    pub last_set_size: Option<SizeV>,
126    pub pending_resize: Option<(u32, u32, f64)>,
127    pub last_render: Instant,
128}
129
130impl<X: GXExt> TrackedWindow<X> {
131    pub fn handle_update(
132        &mut self,
133        rt: &tokio::runtime::Handle,
134        id: ExprId,
135        v: &Value,
136    ) -> Result<()> {
137        if id == self.window_ref.id {
138            self.window_ref.last = Some(v.clone());
139            let resolved = rt
140                .block_on(ResolvedWindow::compile(self.gx.clone(), v.clone()))
141                .context("window ref recompile")?;
142            self.title = resolved.title;
143            self.size = resolved.size;
144            self.theme = resolved.theme;
145            self.icon = resolved.icon;
146            self.decoded_icon = resolved.decoded_icon;
147            self.content_ref = resolved.content_ref;
148            self.content = resolved.content;
149            if let Some(t) = self.title.t.as_ref() {
150                self.window.set_title(t);
151            }
152            if let Some(sz) = self.size.t.as_ref() {
153                let _ = self.window.request_inner_size(winit::dpi::LogicalSize::new(
154                    sz.0.width,
155                    sz.0.height,
156                ));
157            }
158            self.window.set_window_icon(self.decoded_icon.clone());
159            self.needs_redraw = true;
160            return Ok(());
161        }
162        let mut changed = false;
163        if let Some(t) = self.title.update(id, v).context("window update title")? {
164            self.window.set_title(t);
165            changed = true;
166        }
167        if let Some(sz) = self.size.update(id, v).context("window update size")? {
168            if self.last_set_size.take() != Some(*sz) {
169                let _ = self.window.request_inner_size(winit::dpi::LogicalSize::new(
170                    sz.0.width,
171                    sz.0.height,
172                ));
173            }
174            changed = true;
175        }
176        if self.theme.update(id, v).context("window update theme")?.is_some() {
177            changed = true;
178        }
179        if self.icon.update(id, v).context("window update icon")?.is_some() {
180            self.decoded_icon =
181                self.icon.t.as_ref().and_then(|s: &ImageSourceV| match s.decode_icon() {
182                    Ok(i) => i,
183                    Err(e) => {
184                        log::warn!("failed to decode window icon: {e}");
185                        None
186                    }
187                });
188            self.window.set_window_icon(self.decoded_icon.clone());
189            changed = true;
190        }
191        if id == self.content_ref.id {
192            self.content_ref.last = Some(v.clone());
193            self.content = rt
194                .block_on(compile(self.gx.clone(), v.clone()))
195                .context("window content recompile")?;
196            changed = true;
197        }
198        changed |= self.content.handle_update(rt, id, v)?;
199        self.needs_redraw |= changed;
200        Ok(())
201    }
202
203    pub fn window_id(&self) -> WindowId {
204        self.window.id()
205    }
206
207    pub fn iced_theme(&self) -> crate::theme::GraphixTheme {
208        self.theme.t.as_ref().map(|t| t.0.clone()).unwrap_or(crate::theme::GraphixTheme {
209            inner: iced_core::Theme::Dark,
210            overrides: None,
211        })
212    }
213
214    pub fn push_event(&mut self, event: iced_core::Event) {
215        self.pending_events.push(event);
216        self.needs_redraw = true;
217    }
218
219    pub fn editor_action(
220        &mut self,
221        id: ExprId,
222        action: &iced_widget::text_editor::Action,
223    ) -> Option<(graphix_rt::CallableId, Value)> {
224        self.content.editor_action(id, action)
225    }
226
227    pub fn cursor(&self) -> mouse::Cursor {
228        mouse::Cursor::Available(self.cursor_position)
229    }
230}