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 = icon
47            .t
48            .as_ref()
49            .and_then(|s: &ImageSourceV| match s.decode_icon() {
50                Ok(i) => i,
51                Err(e) => {
52                    log::warn!("failed to decode window icon: {e}");
53                    None
54                }
55            });
56        Ok(Self {
57            gx,
58            title: TRef::new(title).context("window tref title")?,
59            size: TRef::new(size).context("window tref size")?,
60            theme: TRef::new(theme).context("window tref theme")?,
61            icon,
62            decoded_icon,
63            content_ref,
64            content: compiled_content,
65        })
66    }
67
68    /// Build winit WindowAttributes from the resolved title/size refs.
69    pub fn window_attrs(&self) -> WindowAttributes {
70        let title = self.title.t.as_ref().map(|t| t.as_str()).unwrap_or("Graphix");
71        let (w, h) = self
72            .size
73            .t
74            .as_ref()
75            .map(|sz| (sz.0.width, sz.0.height))
76            .unwrap_or((800.0, 600.0));
77        WindowAttributes::default()
78            .with_title(title)
79            .with_inner_size(winit::dpi::LogicalSize::new(w, h))
80            .with_window_icon(self.decoded_icon.clone())
81    }
82
83    /// Consume self and attach an OS window, producing a TrackedWindow.
84    pub fn into_tracked(
85        self,
86        window_ref: Ref<X>,
87        window: Arc<Window>,
88    ) -> TrackedWindow<X> {
89        TrackedWindow {
90            window_ref,
91            gx: self.gx,
92            window,
93            title: self.title,
94            size: self.size,
95            theme: self.theme,
96            icon: self.icon,
97            decoded_icon: self.decoded_icon,
98            content_ref: self.content_ref,
99            content: self.content,
100            cursor_position: iced_core::Point::ORIGIN,
101            last_mouse_interaction: mouse::Interaction::default(),
102            pending_events: Vec::new(),
103            needs_redraw: true,
104            last_set_size: None,
105            pending_resize: None,
106            last_render: Instant::now(),
107        }
108    }
109}
110
111/// Per-window state tracking.
112pub struct TrackedWindow<X: GXExt> {
113    pub window_ref: Ref<X>,
114    pub gx: GXHandle<X>,
115    pub window: Arc<Window>,
116    pub title: TRef<X, String>,
117    pub size: TRef<X, SizeV>,
118    pub theme: TRef<X, ThemeV>,
119    pub icon: TRef<X, ImageSourceV>,
120    pub decoded_icon: Option<winit::window::Icon>,
121    pub content_ref: Ref<X>,
122    pub content: GuiW<X>,
123    pub cursor_position: iced_core::Point,
124    pub last_mouse_interaction: mouse::Interaction,
125    pub pending_events: Vec<iced_core::Event>,
126    pub needs_redraw: bool,
127    pub last_set_size: Option<SizeV>,
128    pub pending_resize: Option<(u32, u32, f64)>,
129    pub last_render: Instant,
130}
131
132impl<X: GXExt> TrackedWindow<X> {
133    pub fn handle_update(
134        &mut self,
135        rt: &tokio::runtime::Handle,
136        id: ExprId,
137        v: &Value,
138    ) -> Result<()> {
139        if id == self.window_ref.id {
140            self.window_ref.last = Some(v.clone());
141            let resolved = rt
142                .block_on(ResolvedWindow::compile(self.gx.clone(), v.clone()))
143                .context("window ref recompile")?;
144            self.title = resolved.title;
145            self.size = resolved.size;
146            self.theme = resolved.theme;
147            self.icon = resolved.icon;
148            self.decoded_icon = resolved.decoded_icon;
149            self.content_ref = resolved.content_ref;
150            self.content = resolved.content;
151            if let Some(t) = self.title.t.as_ref() {
152                self.window.set_title(t);
153            }
154            if let Some(sz) = self.size.t.as_ref() {
155                let _ = self.window.request_inner_size(winit::dpi::LogicalSize::new(
156                    sz.0.width,
157                    sz.0.height,
158                ));
159            }
160            self.window.set_window_icon(self.decoded_icon.clone());
161            self.needs_redraw = true;
162            return Ok(());
163        }
164        let mut changed = false;
165        if let Some(t) = self.title.update(id, v).context("window update title")? {
166            self.window.set_title(t);
167            changed = true;
168        }
169        if let Some(sz) = self.size.update(id, v).context("window update size")? {
170            if self.last_set_size.take() != Some(*sz) {
171                let _ = self.window.request_inner_size(winit::dpi::LogicalSize::new(
172                    sz.0.width,
173                    sz.0.height,
174                ));
175            }
176            changed = true;
177        }
178        if self.theme.update(id, v).context("window update theme")?.is_some() {
179            changed = true;
180        }
181        if self.icon.update(id, v).context("window update icon")?.is_some() {
182            self.decoded_icon = self.icon.t.as_ref().and_then(
183                |s: &ImageSourceV| match s.decode_icon() {
184                    Ok(i) => i,
185                    Err(e) => {
186                        log::warn!("failed to decode window icon: {e}");
187                        None
188                    }
189                },
190            );
191            self.window.set_window_icon(self.decoded_icon.clone());
192            changed = true;
193        }
194        if id == self.content_ref.id {
195            self.content_ref.last = Some(v.clone());
196            self.content = rt
197                .block_on(compile(self.gx.clone(), v.clone()))
198                .context("window content recompile")?;
199            changed = true;
200        }
201        changed |= self.content.handle_update(rt, id, v)?;
202        self.needs_redraw |= changed;
203        Ok(())
204    }
205
206    pub fn window_id(&self) -> WindowId {
207        self.window.id()
208    }
209
210    pub fn iced_theme(&self) -> crate::theme::GraphixTheme {
211        self.theme.t.as_ref().map(|t| t.0.clone()).unwrap_or(crate::theme::GraphixTheme {
212            inner: iced_core::Theme::Dark,
213            overrides: None,
214        })
215    }
216
217    pub fn push_event(&mut self, event: iced_core::Event) {
218        self.pending_events.push(event);
219        self.needs_redraw = true;
220    }
221
222    pub fn editor_action(
223        &mut self,
224        id: ExprId,
225        action: &iced_widget::text_editor::Action,
226    ) -> Option<(graphix_rt::CallableId, Value)> {
227        self.content.editor_action(id, action)
228    }
229
230    pub fn cursor(&self) -> mouse::Cursor {
231        mouse::Cursor::Available(self.cursor_position)
232    }
233}