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