Skip to main content

densa_window/
lib.rs

1//! Runs a densa interface in a native window.
2//!
3//! [`run`] opens a window, creates a [`Ui`] for it and hands it to an [`App`]
4//! to fill with widgets. From then on it translates the window system's input (winit) into
5//! `densa_ui` events, passes the actions widgets report to the app, and paints the interface
6//! with `densa_render` (wgpu) in one draw call per frame, presented with V-Sync.
7//!
8//! Frames are painted when something changed, when another thread asks for one through a
9//! [`Waker`] (new data arrived), or continuously while the app asks for it (animation).
10//!
11//! Widgets copy and paste through the system clipboard (on Linux, X11's; XWayland's under
12//! Wayland). Files dragged onto the window reach [`App::file_drop`].
13//!
14//! Screen readers (Narrator, VoiceOver, Orca) read the interface through AccessKit: the
15//! window sends the [`Ui`]'s accessibility tree after each frame, only while one is
16//! listening, and carries out what it asks.
17//!
18//! The [`Ui`]'s text uses `densa_paragraph::TextSystem::new`: the bundled font, and the
19//! system's fonts for characters it lacks (Korean, Japanese, Chinese, ...). Turning off the
20//! default `system-fonts` feature leaves the bundled font alone.
21//!
22//! ```no_run
23//! use std::error::Error;
24//! use densa_ui::{Ui, Style};
25//! use densa_window::{App, Host, WindowOptions};
26//!
27//! struct Hello;
28//!
29//! impl App for Hello {
30//!     fn build(&mut self, ui: &mut Ui, host: &mut Host<'_>) -> Result<(), Box<dyn Error>> {
31//!         // Add widgets under ui.root() here; hand host.waker() to threads that feed them.
32//!         let _ = (ui.root(), host.waker());
33//!         Ok(())
34//!     }
35//! }
36//!
37//! densa_window::run(WindowOptions::new("Hello"), Hello)?;
38//! # Ok::<(), Box<dyn Error>>(())
39//! ```
40
41mod access;
42mod clipboard;
43mod gpu;
44mod translate;
45
46use std::error::Error;
47use std::fmt;
48use std::path::PathBuf;
49use std::sync::Arc;
50use std::time::Instant;
51
52use densa_core::{Point, Size};
53use densa_draw::{Canvas, InstanceBuffer};
54use densa_paragraph::TextSystem;
55use densa_text::{AtlasConfig, GlyphCache};
56use densa_ui::{Action, Cursor, KeyEvent, PointerEvent, PointerKind, Theme, Ui};
57use winit::application::ApplicationHandler;
58use winit::dpi::{LogicalSize, PhysicalPosition, PhysicalSize};
59use winit::event::{ElementState, WindowEvent};
60use winit::event_loop::{ActiveEventLoop, EventLoop, EventLoopProxy};
61use winit::keyboard::ModifiersState;
62use winit::window::{Window, WindowId};
63
64use crate::gpu::Gpu;
65
66/// An application: it builds its interface once, then reacts to what widgets report and
67/// updates live content before each frame. Every call gets the [`Host`]: the window it runs
68/// in.
69pub trait App: 'static {
70    /// Fills the interface: called once, when the window and its [`Ui`] exist.
71    ///
72    /// # Errors
73    /// Any error stops [`run`], which returns it.
74    fn build(&mut self, ui: &mut Ui, host: &mut Host<'_>) -> Result<(), Box<dyn Error>>;
75
76    /// Handles an action a widget reported (a click, an edit), right after the input that
77    /// caused it.
78    fn action(&mut self, ui: &mut Ui, host: &mut Host<'_>, action: Action) {
79        let _ = (ui, host, action);
80    }
81
82    /// Handles a file dragged over the window or dropped on it (one call per file). Nothing
83    /// happens unless the app does something with it.
84    fn file_drop(&mut self, ui: &mut Ui, host: &mut Host<'_>, drop: FileDrop) {
85        let _ = (ui, host, drop);
86    }
87
88    /// Updates live content before a frame is painted. Return `true` to keep painting frames
89    /// continuously (animation), `false` to paint only when something changes or a
90    /// [`Waker`] asks for a frame.
91    fn frame(&mut self, ui: &mut Ui, host: &mut Host<'_>) -> bool {
92        let _ = (ui, host);
93        false
94    }
95}
96
97/// A file dragged over the window, or dropped on it: what [`App::file_drop`] is told.
98///
99/// A drag ends either with [`Left`](Self::Left) (dragged away) or with a [`Dropped`](Self::Dropped)
100/// for each file (no `Left` then). Windows, macOS and X11 report file drops; Wayland does
101/// not yet (winit 0.30). [`WindowOptions::with_file_drops`] turns them off.
102#[derive(Debug, Clone, PartialEq, Eq)]
103#[non_exhaustive]
104pub enum FileDrop {
105    /// A file is held over the window (once for each file held).
106    Hovering(PathBuf),
107    /// The files held over the window left it without being dropped.
108    Left,
109    /// A file was dropped on the window (once for each file dropped).
110    Dropped(PathBuf),
111}
112
113/// The window an [`App`] runs in.
114#[derive(Debug)]
115pub struct Host<'a> {
116    window: &'a Window,
117    proxy: &'a EventLoopProxy<UserEvent>,
118    close: &'a mut bool,
119}
120
121impl Host<'_> {
122    /// A handle any thread can use to ask for a frame: a feed thread calls
123    /// [`Waker::wake`] after publishing new data.
124    pub fn waker(&self) -> Waker {
125        Waker(self.proxy.clone())
126    }
127
128    /// Sets the window's title.
129    pub fn set_title(&self, title: &str) {
130        self.window.set_title(title);
131    }
132
133    /// Closes the window once the current call returns, which ends [`run`].
134    pub fn close(&mut self) {
135        *self.close = true;
136    }
137}
138
139/// Asks the window's thread for a frame, from any thread.
140#[derive(Debug, Clone)]
141pub struct Waker(EventLoopProxy<UserEvent>);
142
143impl Waker {
144    /// Asks for a frame: [`App::frame`] runs and the interface is painted soon. `false` if
145    /// the window is gone.
146    pub fn wake(&self) -> bool {
147        self.0.send_event(UserEvent::Wake).is_ok()
148    }
149}
150
151/// What other threads send the window's thread: a [`Waker`]'s request for a frame, or a
152/// screen reader's request through AccessKit.
153#[derive(Debug)]
154enum UserEvent {
155    Wake,
156    Access(accesskit_winit::Event),
157}
158
159impl From<accesskit_winit::Event> for UserEvent {
160    fn from(event: accesskit_winit::Event) -> Self {
161        Self::Access(event)
162    }
163}
164
165/// The window [`run`] opens.
166#[derive(Debug, Clone)]
167#[non_exhaustive]
168pub struct WindowOptions {
169    /// The window's title.
170    pub title: String,
171    /// The window's inner size, in logical pixels.
172    pub size: Size,
173    /// Colors and sizes of the widgets.
174    pub theme: Theme,
175    /// Rectangles and glyphs a frame can hold; more are dropped (and counted).
176    pub instances: usize,
177    /// The graphics API that draws the window.
178    pub backend: Backend,
179    /// Whether files dragged onto the window reach [`App::file_drop`] (on by default). Off,
180    /// the window on Windows does not set up OLE drag and drop, which fails if the thread
181    /// already uses COM in its multithreaded mode.
182    pub file_drops: bool,
183}
184
185/// The graphics API a window draws with. The `WGPU_BACKEND` environment variable
186/// (`vulkan`, `dx12`, `metal`, `gl`, comma-separated) overrides it, to try another without
187/// rebuilding. When the chosen API gives no device, the window tries every API before
188/// [`run`] fails.
189#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
190#[non_exhaustive]
191pub enum Backend {
192    /// Whichever API the system offers best: Vulkan, Metal or DirectX 12, and OpenGL where
193    /// none of those is there.
194    #[default]
195    Auto,
196    /// Vulkan, on Linux and Windows.
197    Vulkan,
198    /// Metal, on macOS.
199    Metal,
200    /// DirectX 12, on Windows.
201    Dx12,
202    /// OpenGL, or OpenGL ES. On some drivers it takes much less memory than Vulkan or
203    /// DirectX 12 for an interface that draws little: measure on the machines you target.
204    Gl,
205}
206
207impl WindowOptions {
208    /// A 1280x800 window titled `title`, with the default theme.
209    pub fn new(title: impl Into<String>) -> Self {
210        Self {
211            title: title.into(),
212            size: Size::new(1280.0, 800.0),
213            theme: Theme::default(),
214            instances: 65_536,
215            backend: Backend::Auto,
216            file_drops: true,
217        }
218    }
219
220    /// The options with an inner size in logical pixels.
221    #[must_use]
222    pub fn with_size(mut self, width: f32, height: f32) -> Self {
223        self.size = Size::new(width, height);
224        self
225    }
226
227    /// The options with `theme`.
228    #[must_use]
229    pub fn with_theme(mut self, theme: Theme) -> Self {
230        self.theme = theme;
231        self
232    }
233
234    /// The options with files dropped on the window reaching the app, or not; see
235    /// [`WindowOptions::file_drops`].
236    #[must_use]
237    pub fn with_file_drops(mut self, file_drops: bool) -> Self {
238        self.file_drops = file_drops;
239        self
240    }
241
242    /// The options drawing with `backend`; see [`Backend`].
243    #[must_use]
244    pub fn with_backend(mut self, backend: Backend) -> Self {
245        self.backend = backend;
246        self
247    }
248
249    /// The options holding up to `instances` rectangles and glyphs a frame.
250    #[must_use]
251    pub fn with_instances(mut self, instances: usize) -> Self {
252        self.instances = instances;
253        self
254    }
255}
256
257/// Why [`run`] stopped with an error.
258#[derive(Debug)]
259#[non_exhaustive]
260pub enum WindowError {
261    /// The window system's event loop or window failed.
262    Window(String),
263    /// The GPU could not be set up or drawn to.
264    Gpu(String),
265    /// The fonts could not be loaded.
266    Text(String),
267    /// The application's [`App::build`] failed.
268    App(Box<dyn Error>),
269}
270
271impl fmt::Display for WindowError {
272    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
273        match self {
274            Self::Window(message) => write!(f, "window system: {message}"),
275            Self::Gpu(message) => write!(f, "GPU: {message}"),
276            Self::Text(message) => write!(f, "fonts: {message}"),
277            Self::App(error) => write!(f, "application: {error}"),
278        }
279    }
280}
281
282impl Error for WindowError {
283    fn source(&self) -> Option<&(dyn Error + 'static)> {
284        match self {
285            Self::App(error) => Some(error.as_ref()),
286            _ => None,
287        }
288    }
289}
290
291/// Opens a window for `app` and runs until it is closed.
292///
293/// # Errors
294/// [`WindowError`] if the window, the GPU or the fonts cannot be set up, a frame cannot be
295/// drawn, or [`App::build`] fails.
296pub fn run(options: WindowOptions, app: impl App) -> Result<(), WindowError> {
297    let event_loop = EventLoop::<UserEvent>::with_user_event()
298        .build()
299        .map_err(|e| WindowError::Window(e.to_string()))?;
300    let mut shell = Shell {
301        app,
302        options,
303        proxy: event_loop.create_proxy(),
304        session: None,
305        error: None,
306        close: false,
307    };
308    event_loop
309        .run_app(&mut shell)
310        .map_err(|e| WindowError::Window(e.to_string()))?;
311    shell.error.map_or(Ok(()), Err)
312}
313
314struct Shell<A: App> {
315    app: A,
316    options: WindowOptions,
317    proxy: EventLoopProxy<UserEvent>,
318    session: Option<Session>,
319    error: Option<WindowError>,
320    /// The app asked to close the window while building its interface.
321    close: bool,
322}
323
324/// The window, its GPU surface and its interface.
325struct Session {
326    window: Arc<Window>,
327    /// Hands the interface to screen readers, and their requests back.
328    access: accesskit_winit::Adapter,
329    gpu: Gpu,
330    ui: Ui,
331    cache: GlyphCache,
332    instances: InstanceBuffer,
333    start: Instant,
334    pointer: Point,
335    modifiers: ModifiersState,
336    cursor: Cursor,
337    continuous: bool,
338}
339
340impl<A: App> Shell<A> {
341    fn fail(&mut self, event_loop: &ActiveEventLoop, error: WindowError) {
342        self.error = Some(error);
343        event_loop.exit();
344    }
345
346    fn open(&mut self, event_loop: &ActiveEventLoop) -> Result<Session, WindowError> {
347        let size = self.options.size;
348        // Hidden until AccessKit's adapter is attached, which must happen before it is shown.
349        let attributes = Window::default_attributes()
350            .with_title(self.options.title.clone())
351            .with_inner_size(LogicalSize::new(size.width, size.height))
352            .with_visible(false);
353        #[cfg(windows)]
354        let attributes = {
355            use winit::platform::windows::WindowAttributesExtWindows;
356            attributes.with_drag_and_drop(self.options.file_drops)
357        };
358        let window = event_loop
359            .create_window(attributes)
360            .map_err(|e| WindowError::Window(e.to_string()))?;
361        let access = accesskit_winit::Adapter::with_event_loop_proxy(
362            event_loop,
363            &window,
364            self.proxy.clone(),
365        );
366        let window = Arc::new(window);
367        window.set_ime_allowed(true);
368        let mut cache = GlyphCache::new(AtlasConfig::DEFAULT);
369        let gpu = Gpu::new(
370            event_loop,
371            &window,
372            self.options.instances,
373            &mut cache,
374            self.options.backend,
375        )?;
376        let text = TextSystem::new().map_err(|e| WindowError::Text(e.to_string()))?;
377        let mut ui = Ui::new(text);
378        ui.set_theme(self.options.theme.clone());
379        ui.set_clipboard(clipboard::SystemClipboard::default());
380        ui.set_scale(window.scale_factor() as f32);
381        let inner = window.inner_size();
382        ui.resize(Size::new(inner.width as f32, inner.height as f32));
383        let mut host = Host {
384            window: &window,
385            proxy: &self.proxy,
386            close: &mut self.close,
387        };
388        self.app
389            .build(&mut ui, &mut host)
390            .map_err(WindowError::App)?;
391        window.set_visible(true);
392        Ok(Session {
393            window,
394            access,
395            gpu,
396            ui,
397            cache,
398            instances: InstanceBuffer::with_capacity(self.options.instances),
399            start: Instant::now(),
400            pointer: Point::ORIGIN,
401            modifiers: ModifiersState::empty(),
402            cursor: Cursor::Default,
403            continuous: false,
404        })
405    }
406}
407
408impl<A: App> ApplicationHandler<UserEvent> for Shell<A> {
409    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
410        if self.session.is_some() {
411            return;
412        }
413        match self.open(event_loop) {
414            Ok(session) => {
415                session.window.request_redraw();
416                self.session = Some(session);
417                if self.close {
418                    event_loop.exit();
419                }
420            }
421            Err(error) => self.fail(event_loop, error),
422        }
423    }
424
425    fn window_event(&mut self, event_loop: &ActiveEventLoop, _: WindowId, event: WindowEvent) {
426        let Some(session) = self.session.as_mut() else {
427            return;
428        };
429        let window = Arc::clone(&session.window);
430        session.access.process_event(&window, &event);
431        let redraw = matches!(event, WindowEvent::RedrawRequested);
432        let mut close = false;
433        let mut failed = None;
434        {
435            let mut host = Host {
436                window: &window,
437                proxy: &self.proxy,
438                close: &mut close,
439            };
440            match event {
441                WindowEvent::CloseRequested => host.close(),
442                WindowEvent::RedrawRequested => {
443                    session.continuous = self.app.frame(&mut session.ui, &mut host);
444                    failed = session.paint().err();
445                }
446                other => match translate::file_drop(&other) {
447                    Some(drop) if self.options.file_drops => {
448                        self.app.file_drop(&mut session.ui, &mut host, drop);
449                        // What the app showed for it is painted.
450                        window.request_redraw();
451                    }
452                    // Turned off: not the app's business.
453                    Some(_) => {}
454                    None => session.input(&other),
455                },
456            }
457            if failed.is_none() {
458                while let Some(action) = session.ui.next_action() {
459                    self.app.action(&mut session.ui, &mut host, action);
460                }
461                session.reflect();
462                // After a frame and what the app did about it: the tree screen readers see
463                // matches the screen.
464                if redraw {
465                    session.publish_access();
466                }
467            }
468        }
469        if let Some(error) = failed {
470            self.fail(event_loop, error);
471        } else if close {
472            event_loop.exit();
473        }
474    }
475
476    fn user_event(&mut self, event_loop: &ActiveEventLoop, event: UserEvent) {
477        let Some(session) = self.session.as_mut() else {
478            return;
479        };
480        let access = match event {
481            UserEvent::Wake => {
482                session.window.request_redraw();
483                return;
484            }
485            UserEvent::Access(access) => access.window_event,
486        };
487        match access {
488            accesskit_winit::WindowEvent::InitialTreeRequested => session.publish_access(),
489            accesskit_winit::WindowEvent::ActionRequested(request) => {
490                access::perform(&mut session.ui, &request);
491                let window = Arc::clone(&session.window);
492                let mut close = false;
493                let mut host = Host {
494                    window: &window,
495                    proxy: &self.proxy,
496                    close: &mut close,
497                };
498                while let Some(action) = session.ui.next_action() {
499                    self.app.action(&mut session.ui, &mut host, action);
500                }
501                session.reflect();
502                session.publish_access();
503                if close {
504                    event_loop.exit();
505                }
506            }
507            accesskit_winit::WindowEvent::AccessibilityDeactivated => {}
508        }
509    }
510
511    fn about_to_wait(&mut self, _: &ActiveEventLoop) {
512        if let Some(session) = &self.session {
513            if session.continuous {
514                session.window.request_redraw();
515            }
516        }
517    }
518}
519
520impl Session {
521    /// Hands a window event to the interface.
522    fn input(&mut self, event: &WindowEvent) {
523        let modifiers = translate::modifiers(self.modifiers);
524        let at = |kind: PointerKind, position: Point, start: Instant| {
525            PointerEvent::new(kind, position)
526                .with_modifiers(modifiers)
527                .at(start.elapsed())
528        };
529        match event {
530            WindowEvent::Resized(size) => {
531                self.gpu.resize(size.width, size.height);
532                let size = Size::new(size.width as f32, size.height as f32);
533                // Menus are placed against what they opened from, which may have moved; the
534                // event also comes when the window is first shown, at the same size.
535                if size != self.ui.viewport() {
536                    self.ui.close_popups();
537                }
538                self.ui.resize(size);
539            }
540            WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
541                // Popups were placed in the old pixels.
542                self.ui.close_popups();
543                self.ui.set_scale(*scale_factor as f32);
544            }
545            WindowEvent::ModifiersChanged(state) => self.modifiers = state.state(),
546            WindowEvent::CursorMoved { position, .. } => {
547                self.pointer = Point::new(position.x as f32, position.y as f32);
548                self.ui
549                    .pointer(at(PointerKind::Move, self.pointer, self.start));
550            }
551            WindowEvent::CursorLeft { .. } => self.ui.pointer_left(self.pointer),
552            // Alt+Tab mid-drag: no release will come, and modifiers may be released elsewhere.
553            WindowEvent::Focused(false) => {
554                self.ui.cancel_pointer();
555                // As native menus do when another window is chosen.
556                self.ui.close_popups();
557                self.modifiers = ModifiersState::empty();
558            }
559            WindowEvent::MouseInput { state, button, .. } => {
560                let button = translate::button(*button);
561                let kind = match state {
562                    ElementState::Pressed => PointerKind::Down(button),
563                    ElementState::Released => PointerKind::Up(button),
564                };
565                self.ui.pointer(at(kind, self.pointer, self.start));
566            }
567            WindowEvent::MouseWheel { delta, .. } => {
568                let kind = PointerKind::Scroll(translate::scroll(*delta));
569                self.ui.pointer(at(kind, self.pointer, self.start));
570            }
571            WindowEvent::KeyboardInput {
572                event,
573                is_synthetic: false,
574                ..
575            } => {
576                let Some(key) = translate::key(&event.logical_key) else {
577                    return;
578                };
579                let mut key = if event.state == ElementState::Pressed {
580                    KeyEvent::down(key)
581                } else {
582                    KeyEvent::up(key)
583                }
584                .with_modifiers(modifiers);
585                if let Some(text) = translate::text(event.text.as_deref()) {
586                    key = key.with_text(text);
587                }
588                if event.repeat {
589                    key = key.repeated();
590                }
591                self.ui.key(&key);
592            }
593            WindowEvent::Ime(ime) => {
594                self.ui.ime(&translate::ime(ime));
595            }
596            _ => {}
597        }
598    }
599
600    /// Paints the interface and presents it.
601    fn paint(&mut self) -> Result<(), WindowError> {
602        {
603            let mut canvas = Canvas::begin(&mut self.cache, &mut self.instances);
604            self.ui.paint(&mut canvas);
605        }
606        let background = self.ui.theme().background;
607        self.gpu.draw(
608            &self.window,
609            &mut self.cache,
610            self.instances.as_slice(),
611            background,
612        )?;
613        Ok(())
614    }
615
616    /// Sends screen readers the interface as it is now; nothing is built unless one is
617    /// listening.
618    fn publish_access(&mut self) {
619        let Self {
620            access, ui, window, ..
621        } = self;
622        access.update_if_active(|| access::tree_update(&ui.access_tree(), &window.title()));
623    }
624
625    /// Shows the interface's state in the window: cursor, input method area, repaint.
626    fn reflect(&mut self) {
627        let cursor = self.ui.cursor();
628        if cursor != self.cursor {
629            self.cursor = cursor;
630            self.window.set_cursor(translate::cursor(cursor));
631        }
632        if let Some(area) = self.ui.ime_area() {
633            self.window.set_ime_cursor_area(
634                PhysicalPosition::new(area.left(), area.top()),
635                PhysicalSize::new(area.width().max(1.0), area.height().max(1.0)),
636            );
637        }
638        if self.ui.needs_paint() {
639            self.window.request_redraw();
640        }
641    }
642}