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