Skip to main content

gpui_rhai/
window.rs

1use std::cell::RefCell;
2use std::collections::{BTreeMap, VecDeque};
3use std::rc::{Rc, Weak};
4
5use thiserror::Error;
6
7use crate::ScriptCallback;
8
9const MAX_WINDOWS: usize = 16;
10const MAX_PENDING_COMMANDS: usize = 64;
11
12#[derive(Clone, Debug, PartialEq)]
13pub struct ScriptWindowSpec {
14    pub id: String,
15    pub title: String,
16    pub width: f64,
17    pub height: f64,
18    pub focus: bool,
19}
20
21impl ScriptWindowSpec {
22    /// Validate a script-visible window declaration.
23    ///
24    /// # Errors
25    ///
26    /// Returns [`WindowCommandError`] for unsafe identifiers, titles, or bounds.
27    pub fn validate(&self) -> Result<(), WindowCommandError> {
28        if !valid_window_id(&self.id) {
29            return Err(WindowCommandError::InvalidId(self.id.clone()));
30        }
31        if self.title.trim().is_empty() || self.title.chars().count() > 256 {
32            return Err(WindowCommandError::InvalidTitle);
33        }
34        if !valid_dimension(self.width) || !valid_dimension(self.height) {
35            return Err(WindowCommandError::InvalidBounds {
36                width: self.width,
37                height: self.height,
38            });
39        }
40        Ok(())
41    }
42}
43
44fn valid_window_id(id: &str) -> bool {
45    (1..=64).contains(&id.len())
46        && id
47            .chars()
48            .next()
49            .is_some_and(|character| character.is_ascii_alphanumeric())
50        && id
51            .chars()
52            .all(|character| character.is_ascii_alphanumeric() || matches!(character, '_' | '-'))
53}
54
55fn valid_dimension(value: f64) -> bool {
56    value.is_finite() && (200.0..=4096.0).contains(&value)
57}
58
59#[derive(Clone, Debug, PartialEq)]
60pub enum WindowCommand {
61    Open(ScriptWindowSpec),
62    Focus(String),
63    Close(String),
64}
65
66impl WindowCommand {
67    fn target(&self) -> &str {
68        match self {
69            Self::Open(spec) => &spec.id,
70            Self::Focus(id) | Self::Close(id) => id,
71        }
72    }
73}
74
75/// A command and the qualifications captured when it was enqueued.
76///
77/// A native pump must not replace this origin with its own authority. Rust
78/// request methods without a source explicitly use trusted Host origin.
79#[derive(Clone, Debug)]
80pub struct QueuedWindowCommand {
81    command: WindowCommand,
82    origin: WindowCommandOrigin,
83    target: Rc<WindowIdentity>,
84}
85
86impl QueuedWindowCommand {
87    #[must_use]
88    pub const fn command(&self) -> &WindowCommand {
89        &self.command
90    }
91
92    pub(crate) fn source_binding(&self) -> Option<NativeWindowBinding> {
93        match &self.origin {
94            WindowCommandOrigin::Host => None,
95            WindowCommandOrigin::View { identity, .. } => identity.native.borrow().clone(),
96        }
97    }
98
99    pub(crate) fn is_host_origin(&self) -> bool {
100        matches!(self.origin, WindowCommandOrigin::Host)
101    }
102
103    pub(crate) fn target_binding(&self) -> Option<NativeWindowBinding> {
104        self.target.native.borrow().clone()
105    }
106}
107
108#[derive(Clone, Debug)]
109enum WindowCommandOrigin {
110    Host,
111    View {
112        id: String,
113        identity: Rc<WindowIdentity>,
114    },
115}
116
117#[derive(Debug, Default)]
118struct WindowIdentity {
119    mount: RefCell<Option<Weak<()>>>,
120    native: RefCell<Option<NativeWindowBinding>>,
121}
122
123#[derive(Clone, Debug)]
124pub(crate) struct NativeWindowBinding {
125    pub(crate) window: gpui::WindowId,
126    pub(crate) lease: Weak<()>,
127}
128
129#[derive(Clone, Copy, Debug, Eq, PartialEq)]
130pub enum WindowCommandPolicy {
131    ApplicationOwned,
132    Disabled,
133}
134
135#[derive(Clone, Copy, Debug, Eq, PartialEq)]
136enum WindowStatus {
137    Pending,
138    Open,
139}
140
141#[derive(Clone, Debug)]
142struct WindowRecord {
143    status: WindowStatus,
144    policy: WindowCommandPolicy,
145    view_id: String,
146    close_handler: Option<ScriptCallback>,
147    identity: Rc<WindowIdentity>,
148}
149
150#[derive(Clone, Debug, Default)]
151pub struct WindowCommandRegistry {
152    windows: BTreeMap<String, WindowRecord>,
153    commands: VecDeque<QueuedWindowCommand>,
154}
155
156impl WindowCommandRegistry {
157    #[must_use]
158    pub fn new() -> Self {
159        Self::default()
160    }
161
162    /// Register a window already created by the Rust host.
163    ///
164    /// # Errors
165    ///
166    /// Returns duplicate or invalid-ID errors.
167    pub fn register_open(&mut self, id: impl Into<String>) -> Result<(), WindowCommandError> {
168        self.register_open_with_policy(id, WindowCommandPolicy::ApplicationOwned)
169    }
170
171    /// Register an existing host window with an explicit script command policy.
172    ///
173    /// # Errors
174    ///
175    /// Returns duplicate, limit, or invalid-ID errors.
176    pub fn register_open_with_policy(
177        &mut self,
178        id: impl Into<String>,
179        policy: WindowCommandPolicy,
180    ) -> Result<(), WindowCommandError> {
181        let id = id.into();
182        self.register_open_for_view(id.clone(), policy, id)
183    }
184
185    /// Register an existing host window and its owning mounted view.
186    ///
187    /// # Errors
188    ///
189    /// Returns duplicate, limit, or invalid-ID errors.
190    pub fn register_open_for_view(
191        &mut self,
192        id: impl Into<String>,
193        policy: WindowCommandPolicy,
194        view_id: impl Into<String>,
195    ) -> Result<(), WindowCommandError> {
196        let id = id.into();
197        if !valid_window_id(&id) {
198            return Err(WindowCommandError::InvalidId(id));
199        }
200        if self.windows.contains_key(&id) {
201            return Err(WindowCommandError::Duplicate(id));
202        }
203        if self.windows.len() >= MAX_WINDOWS {
204            return Err(WindowCommandError::WindowLimit(MAX_WINDOWS));
205        }
206        self.windows.insert(
207            id,
208            WindowRecord {
209                status: WindowStatus::Open,
210                policy,
211                view_id: view_id.into(),
212                close_handler: None,
213                identity: Rc::new(WindowIdentity::default()),
214            },
215        );
216        Ok(())
217    }
218
219    /// Validate and queue creation of another instance of the script entry.
220    ///
221    /// # Errors
222    ///
223    /// Returns duplicate, limit, queue, or spec validation errors. This Rust
224    /// entry explicitly uses trusted Host origin; scripts use `request_open_from`.
225    pub fn request_open(&mut self, spec: ScriptWindowSpec) -> Result<(), WindowCommandError> {
226        self.enqueue_open(spec, WindowCommandOrigin::Host)
227    }
228
229    fn enqueue_open(
230        &mut self,
231        spec: ScriptWindowSpec,
232        origin: WindowCommandOrigin,
233    ) -> Result<(), WindowCommandError> {
234        spec.validate()?;
235        if self.windows.contains_key(&spec.id) {
236            return Err(WindowCommandError::Duplicate(spec.id));
237        }
238        if self.windows.len() >= MAX_WINDOWS {
239            return Err(WindowCommandError::WindowLimit(MAX_WINDOWS));
240        }
241        self.require_queue_capacity()?;
242        let identity = Rc::new(WindowIdentity::default());
243        self.windows.insert(
244            spec.id.clone(),
245            WindowRecord {
246                status: WindowStatus::Pending,
247                policy: WindowCommandPolicy::ApplicationOwned,
248                view_id: spec.id.clone(),
249                close_handler: None,
250                identity: Rc::clone(&identity),
251            },
252        );
253        self.commands.push_back(QueuedWindowCommand {
254            command: WindowCommand::Open(spec),
255            origin,
256            target: identity,
257        });
258        Ok(())
259    }
260
261    /// Mark successful native creation of a queued window.
262    ///
263    /// # Errors
264    ///
265    /// Returns unknown-window errors or an invalid transition.
266    pub fn mark_open(&mut self, id: &str) -> Result<(), WindowCommandError> {
267        let record = self
268            .windows
269            .get_mut(id)
270            .ok_or_else(|| WindowCommandError::Unknown(id.to_owned()))?;
271        if record.status != WindowStatus::Pending {
272            return Err(WindowCommandError::AlreadyOpen(id.to_owned()));
273        }
274        record.status = WindowStatus::Open;
275        Ok(())
276    }
277
278    pub fn fail_open(&mut self, id: &str) {
279        if self
280            .windows
281            .get(id)
282            .is_some_and(|record| record.status == WindowStatus::Pending)
283        {
284            self.windows.remove(id);
285        }
286    }
287
288    /// Queue native focus with trusted Rust Host origin. A pending Open in the
289    /// same queue is a valid target and retains the same reservation identity.
290    ///
291    /// # Errors
292    ///
293    /// Returns unknown-window or queue-limit errors.
294    pub fn request_focus(&mut self, id: &str) -> Result<(), WindowCommandError> {
295        self.enqueue_target(
296            WindowCommand::Focus(id.to_owned()),
297            WindowCommandOrigin::Host,
298        )
299    }
300
301    /// Queue focus after verifying that the source view owns window commands.
302    ///
303    /// # Errors
304    ///
305    /// Returns disabled-policy, unknown-window, or queue errors.
306    pub fn request_focus_from(&mut self, source: &str, id: &str) -> Result<(), WindowCommandError> {
307        self.require_commands(source, "focus_window")?;
308        self.enqueue_target(
309            WindowCommand::Focus(id.to_owned()),
310            self.view_origin(source),
311        )
312    }
313
314    /// Queue forced close with trusted Rust Host origin.
315    ///
316    /// # Errors
317    ///
318    /// Returns unknown-window or queue-limit errors.
319    pub fn request_close(&mut self, id: &str) -> Result<(), WindowCommandError> {
320        self.enqueue_target(
321            WindowCommand::Close(id.to_owned()),
322            WindowCommandOrigin::Host,
323        )
324    }
325
326    /// Queue close after verifying that the source view owns window commands.
327    ///
328    /// # Errors
329    ///
330    /// Returns disabled-policy, unknown-window, or queue errors.
331    pub fn request_close_from(&mut self, source: &str, id: &str) -> Result<(), WindowCommandError> {
332        self.require_commands(source, "close_window")?;
333        self.enqueue_target(
334            WindowCommand::Close(id.to_owned()),
335            self.view_origin(source),
336        )
337    }
338
339    /// Queue open after verifying that the source view owns window commands.
340    ///
341    /// # Errors
342    ///
343    /// Returns disabled-policy, validation, duplicate, limit, or queue errors.
344    pub fn request_open_from(
345        &mut self,
346        source: &str,
347        spec: ScriptWindowSpec,
348    ) -> Result<(), WindowCommandError> {
349        self.require_commands(source, "open_window")?;
350        self.enqueue_open(spec, self.view_origin(source))
351    }
352
353    /// Install or clear the current generation's close-request callback.
354    ///
355    /// # Errors
356    ///
357    /// Returns [`WindowCommandError::Unknown`] for an absent window.
358    pub fn set_close_handler(
359        &mut self,
360        id: &str,
361        handler: Option<ScriptCallback>,
362    ) -> Result<(), WindowCommandError> {
363        self.require_commands(id, "set_close_handler")?;
364        let record = self
365            .windows
366            .get_mut(id)
367            .ok_or_else(|| WindowCommandError::Unknown(id.to_owned()))?;
368        record.close_handler = handler;
369        Ok(())
370    }
371
372    #[must_use]
373    pub fn close_handler(&self, id: &str) -> Option<ScriptCallback> {
374        self.windows
375            .get(id)
376            .and_then(|record| record.close_handler.clone())
377    }
378
379    #[must_use]
380    pub fn drain_commands(&mut self) -> Vec<QueuedWindowCommand> {
381        self.commands.drain(..).collect()
382    }
383
384    #[must_use]
385    pub fn contains(&self, id: &str) -> bool {
386        self.windows.contains_key(id)
387    }
388
389    #[must_use]
390    pub fn open_ids(&self) -> Vec<String> {
391        self.windows
392            .iter()
393            .filter(|(_, record)| record.status == WindowStatus::Open)
394            .map(|(id, _)| id.clone())
395            .collect()
396    }
397
398    pub fn remove(&mut self, id: &str) -> bool {
399        let Some(removed) = self.windows.remove(id) else {
400            return false;
401        };
402        let queued = std::mem::take(&mut self.commands);
403        for command in queued {
404            let revoked = matches!(&command.origin, WindowCommandOrigin::View { identity, .. }
405                if Rc::ptr_eq(identity, &removed.identity));
406            if revoked || Rc::ptr_eq(&command.target, &removed.identity) {
407                self.cancel_open(&command);
408            } else {
409                self.commands.push_back(command);
410            }
411        }
412        true
413    }
414
415    fn view_origin(&self, id: &str) -> WindowCommandOrigin {
416        WindowCommandOrigin::View {
417            id: id.to_owned(),
418            identity: Rc::clone(&self.windows[id].identity),
419        }
420    }
421
422    fn enqueue_target(
423        &mut self,
424        command: WindowCommand,
425        origin: WindowCommandOrigin,
426    ) -> Result<(), WindowCommandError> {
427        let target = self
428            .windows
429            .get(command.target())
430            .ok_or_else(|| WindowCommandError::Unknown(command.target().to_owned()))?;
431        let identity = Rc::clone(&target.identity);
432        self.require_queue_capacity()?;
433        self.commands.push_back(QueuedWindowCommand {
434            command,
435            origin,
436            target: identity,
437        });
438        Ok(())
439    }
440
441    pub(crate) fn qualify_mount(
442        &mut self,
443        id: &str,
444        lease: &Weak<()>,
445    ) -> Result<(), WindowCommandError> {
446        let identity = &self
447            .windows
448            .get(id)
449            .ok_or_else(|| WindowCommandError::Unknown(id.to_owned()))?
450            .identity;
451        let mut mount = identity.mount.borrow_mut();
452        if mount
453            .as_ref()
454            .is_some_and(|previous| !previous.ptr_eq(lease))
455        {
456            return Err(WindowCommandError::Duplicate(id.to_owned()));
457        }
458        *mount = Some(lease.clone());
459        Ok(())
460    }
461
462    pub(crate) fn bind_native(
463        &mut self,
464        id: &str,
465        window: gpui::WindowId,
466    ) -> Result<(), WindowCommandError> {
467        let identity = &self
468            .windows
469            .get(id)
470            .ok_or_else(|| WindowCommandError::Unknown(id.to_owned()))?
471            .identity;
472        let lease = identity
473            .mount
474            .borrow()
475            .clone()
476            .ok_or_else(|| WindowCommandError::Unknown(id.to_owned()))?;
477        let mut native = identity.native.borrow_mut();
478        if native
479            .as_ref()
480            .is_some_and(|previous| previous.window != window || !previous.lease.ptr_eq(&lease))
481        {
482            return Err(WindowCommandError::Duplicate(id.to_owned()));
483        }
484        *native = Some(NativeWindowBinding { window, lease });
485        Ok(())
486    }
487
488    pub(crate) fn is_current_target(&self, command: &QueuedWindowCommand) -> bool {
489        self.windows
490            .get(command.command.target())
491            .is_some_and(|record| Rc::ptr_eq(&record.identity, &command.target))
492    }
493
494    pub(crate) fn is_current_origin(&self, command: &QueuedWindowCommand) -> bool {
495        match &command.origin {
496            WindowCommandOrigin::Host => true,
497            WindowCommandOrigin::View { id, identity } => {
498                self.windows.get(id).is_some_and(|record| {
499                    Rc::ptr_eq(&record.identity, identity)
500                        && identity
501                            .mount
502                            .borrow()
503                            .as_ref()
504                            .is_some_and(|lease| lease.strong_count() > 0)
505                })
506            }
507        }
508    }
509
510    pub(crate) fn cancel_open(&mut self, command: &QueuedWindowCommand) {
511        if matches!(command.command, WindowCommand::Open(_))
512            && self.is_current_target(command)
513            && self.windows[command.command.target()].status == WindowStatus::Pending
514        {
515            self.windows.remove(command.command.target());
516        }
517    }
518
519    fn require_commands(
520        &self,
521        source: &str,
522        command: &'static str,
523    ) -> Result<(), WindowCommandError> {
524        let record = self
525            .windows
526            .get(source)
527            .ok_or_else(|| WindowCommandError::Unknown(source.to_owned()))?;
528        if record.policy == WindowCommandPolicy::Disabled {
529            Err(WindowCommandError::UnsupportedInEmbeddedView {
530                window: source.to_owned(),
531                view: record.view_id.clone(),
532                command,
533            })
534        } else {
535            Ok(())
536        }
537    }
538
539    fn require_queue_capacity(&self) -> Result<(), WindowCommandError> {
540        if self.commands.len() < MAX_PENDING_COMMANDS {
541            Ok(())
542        } else {
543            Err(WindowCommandError::QueueLimit(MAX_PENDING_COMMANDS))
544        }
545    }
546}
547
548#[derive(Clone, Debug, Error, PartialEq)]
549pub enum WindowCommandError {
550    #[error("invalid window ID `{0}`")]
551    InvalidId(String),
552    #[error("window title must contain 1 to 256 characters")]
553    InvalidTitle,
554    #[error("window bounds must be finite and between 200 and 4096, got {width}x{height}")]
555    InvalidBounds { width: f64, height: f64 },
556    #[error("window `{0}` is already registered")]
557    Duplicate(String),
558    #[error("window `{0}` is not registered")]
559    Unknown(String),
560    #[error("window `{0}` is already open")]
561    AlreadyOpen(String),
562    #[error("at most {0} script windows may be active")]
563    WindowLimit(usize),
564    #[error("at most {0} window commands may be pending")]
565    QueueLimit(usize),
566    #[error(
567        "window command `{command}` is unavailable for embedded view `{view}` in window `{window}`"
568    )]
569    UnsupportedInEmbeddedView {
570        window: String,
571        view: String,
572        command: &'static str,
573    },
574}
575
576#[cfg(test)]
577mod tests {
578    use super::*;
579
580    fn spec(id: &str) -> ScriptWindowSpec {
581        ScriptWindowSpec {
582            id: id.to_owned(),
583            title: "Settings".to_owned(),
584            width: 640.0,
585            height: 480.0,
586            focus: true,
587        }
588    }
589
590    #[test]
591    fn commands_validate_identity_lifecycle_and_duplicates() {
592        let mut windows = WindowCommandRegistry::new();
593        windows.register_open("main").unwrap();
594        windows.request_open(spec("settings")).unwrap();
595        assert!(matches!(
596            windows.request_open(spec("settings")),
597            Err(WindowCommandError::Duplicate(_))
598        ));
599        windows.request_focus("settings").unwrap();
600        windows.request_close("settings").unwrap();
601        assert_eq!(
602            windows
603                .drain_commands()
604                .into_iter()
605                .map(|queued| queued.command)
606                .collect::<Vec<_>>(),
607            vec![
608                WindowCommand::Open(spec("settings")),
609                WindowCommand::Focus("settings".into()),
610                WindowCommand::Close("settings".into())
611            ]
612        );
613        windows.mark_open("settings").unwrap();
614        windows.request_focus("settings").unwrap();
615        windows.request_close("settings").unwrap();
616        assert_eq!(
617            windows
618                .drain_commands()
619                .into_iter()
620                .map(|queued| queued.command)
621                .collect::<Vec<_>>(),
622            vec![
623                WindowCommand::Focus("settings".to_owned()),
624                WindowCommand::Close("settings".to_owned()),
625            ]
626        );
627    }
628
629    #[test]
630    fn queued_origin_and_target_keep_their_registration_identity() {
631        let mut windows = WindowCommandRegistry::new();
632        let lease = Rc::new(());
633        windows.register_open("source").unwrap();
634        windows
635            .qualify_mount("source", &Rc::downgrade(&lease))
636            .unwrap();
637        windows.request_open_from("source", spec("next")).unwrap();
638        windows.request_focus_from("source", "next").unwrap();
639        let queued = windows.drain_commands();
640        assert!(queued.iter().all(
641            |command| windows.is_current_origin(command) && windows.is_current_target(command)
642        ));
643        windows.remove("source");
644        windows.register_open("source").unwrap();
645        let replacement = Rc::new(());
646        windows
647            .qualify_mount("source", &Rc::downgrade(&replacement))
648            .unwrap();
649        assert!(
650            queued
651                .iter()
652                .all(|command| !windows.is_current_origin(command))
653        );
654        windows.cancel_open(&queued[0]);
655        windows.request_open_from("source", spec("next")).unwrap();
656        assert!(!windows.is_current_target(&queued[1]));
657        windows.cancel_open(&queued[0]);
658        assert!(
659            windows.contains("next"),
660            "old cancellation must not release a newer reservation"
661        );
662    }
663
664    #[test]
665    fn revocation_releases_only_its_queued_open_and_host_origin_is_explicit() {
666        let mut windows = WindowCommandRegistry::new();
667        let lease = Rc::new(());
668        windows.register_open("source").unwrap();
669        windows
670            .qualify_mount("source", &Rc::downgrade(&lease))
671            .unwrap();
672        windows
673            .request_open_from("source", spec("cancelled"))
674            .unwrap();
675        windows.request_open(spec("trusted")).unwrap();
676        windows.remove("source");
677        assert!(!windows.contains("cancelled"));
678        let queued = windows.drain_commands();
679        assert_eq!(queued.len(), 1);
680        assert!(queued[0].is_host_origin());
681        assert!(windows.is_current_origin(&queued[0]));
682        assert!(windows.is_current_target(&queued[0]));
683    }
684
685    #[test]
686    fn invalid_window_specs_never_reserve_an_id() {
687        let mut windows = WindowCommandRegistry::new();
688        let mut invalid = spec("../escape");
689        invalid.width = 20.0;
690        assert!(windows.request_open(invalid).is_err());
691        assert!(!windows.contains("../escape"));
692    }
693
694    #[test]
695    fn embedded_view_policy_rejects_window_commands_at_call_site() {
696        let mut windows = WindowCommandRegistry::new();
697        windows
698            .register_open_with_policy("host", WindowCommandPolicy::Disabled)
699            .unwrap();
700        assert!(matches!(
701            windows.request_open_from("host", spec("settings")),
702            Err(WindowCommandError::UnsupportedInEmbeddedView {
703                command: "open_window",
704                ..
705            })
706        ));
707        assert!(matches!(
708            windows.request_focus_from("host", "host"),
709            Err(WindowCommandError::UnsupportedInEmbeddedView {
710                command: "focus_window",
711                ..
712            })
713        ));
714        assert!(matches!(
715            windows.request_close_from("host", "host"),
716            Err(WindowCommandError::UnsupportedInEmbeddedView {
717                command: "close_window",
718                ..
719            })
720        ));
721        assert!(windows.drain_commands().is_empty());
722    }
723}