quadraui 0.1.3

Cross-platform Rust UI toolkit: write one AppLogic and run it unmodified on TUI, GTK4 (Linux), macOS, and Windows backends
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
//! Public GTK (Cairo + Pango) rasterisers for `quadraui` primitives.
//!
//! Enabled via the `gtk` Cargo feature. Apps depend on `quadraui` with
//! `features = ["gtk"]` and call these `draw_*` functions to paint
//! primitives onto a [`gtk4::cairo::Context`] using a
//! [`pango::Layout`] for text measurement.
//!
//! Per D6 (see `docs/decisions/BACKEND_TRAIT_PROPOSAL.md` §9): primitives own
//! layout, backends rasterise. Most GTK rasterisers in this module
//! compute the primitive's `*Layout` internally because Pango
//! measurement requires the live `pango::Layout` — taking the layout
//! pre-computed would force callers to share that handle through two
//! separate phases. The result the layout would have produced is
//! returned alongside any per-frame hit regions so callers can dispatch
//! clicks.
//!
//! This module is the destination of issue #223 — the per-primitive
//! rasterisers are being lifted out of vimcode (`src/gtk/quadraui_gtk.rs`)
//! and kubeui (private `draw_status_bar` in `kubeui-gtk/src/main.rs`)
//! one primitive at a time. StatusBar is the pilot.

use gtk4::cairo::Context;
use gtk4::pango;

use crate::types::Color;

mod activity_bar;
mod app_font;
pub mod backend;
mod board;
mod chart;
mod command_center;
mod command_line;
mod completions;
mod context_menu;
mod data_table;
mod dialog;
mod diff_view;
mod drop_overlay;
mod editor;
pub mod events;
mod form;
mod image;
mod list;
mod menu_bar;
pub mod menu_overlay;
mod message_list;
mod minimap;
mod multi_section_view;
mod painted_text;
mod palette;
mod panel;
mod pipeline_view;
mod progress;
mod rich_text_popup;
mod run;
mod scrollbar;
pub mod services;
pub mod shell_runner;
mod sidebar_panel;
mod spinner;
mod split;
mod split_tree;
mod status_bar;
mod surface;
mod tab_bar;
mod terminal;
pub mod testing;
mod text_display;
mod text_input;
mod toast;
mod toolbar;
mod tooltip;
mod tree;

// `TabBarHits` is `#[deprecated]` (issue #823) — this re-export stays (a
// dropped re-export is its own breaking change) until the six-method/
// four-backend swap to `TabBarLayout` lands.
#[allow(deprecated)]
pub use crate::primitives::tab_bar::TabBarHits;
pub use activity_bar::{draw_activity_bar, draw_activity_bar_with_style, ACTIVITY_ROW_PX};
pub use backend::GtkBackend;
pub use board::gtk_board_layout;
pub use chart::gtk_chart_layout;
pub use command_center::{draw_command_center, gtk_command_center_layout};
pub use completions::draw_completions;
pub use context_menu::draw_context_menu;
pub use data_table::{draw_data_table, gtk_data_table_layout};
pub use dialog::draw_dialog;
pub use editor::{draw_editor, editor_col_at_x};
pub use events::{wire_da_events, wire_da_events_with_scroll_direction};
pub use form::draw_settings_chrome;
pub use image::draw_image;
pub use list::{draw_list, gtk_list_layout};
pub use menu_bar::{draw_menu_bar, gtk_menu_bar_layout};
pub use menu_overlay::MenuOverlay;
pub use message_list::draw_message_list;
// #738: the legibility/render-mode thresholds (`is_legible`, `render_mode`,
// `minimap_font_px`, `MinimapRenderMode`) and the shared `ROW_PITCH_PX` /
// `COLUMN_CAPACITY` geometry constants moved to `primitives::minimap` so
// `win::minimap` (and any future macOS rasteriser) consume the exact same
// decision logic — see that module's docs. Zero hits for any of those
// names in `~/src/coord-tui/src` or `~/src/vimcode/src`, so they're
// dropped from this re-export list rather than kept as a stale alias
// (CLAUDE.md's "Downstream consumers" rule 8: zero hits ⇒ free to move).
pub use minimap::{draw_minimap, gtk_minimap_layout};
pub use multi_section_view::{
    draw_multi_section_view, gtk_msv_layout, metrics_for as multi_section_view_metrics,
};
pub use palette::{draw_palette, gtk_palette_layout};
pub use panel::gtk_panel_layout;
pub use pipeline_view::gtk_pipeline_view_layout;
pub use progress::gtk_progress_layout;
pub use rich_text_popup::{
    draw_rich_text_popup, RICH_TEXT_POPUP_SB_INSET, RICH_TEXT_POPUP_SB_WIDTH,
};
pub use run::{run, run_with, GtkRunner, RunConfig, StepOutcome};
pub use sidebar_panel::gtk_sidebar_panel_layout;
pub use spinner::{draw_spinner, gtk_spinner_layout};
pub use split::gtk_split_layout;
pub use split_tree::gtk_split_tree_layout;
// `draw_status_bar` stays public and un-deprecated (issue #1251 dropped
// the `#[deprecated]` attribute #1109 had put on it, once `vimcode`'s
// `src/gtk/mod.rs` test helper — its one external caller — migrated
// off it): `kubeui-gtk/src/main.rs` still calls it directly, and has no
// alternative — see `status_bar.rs`'s module doc.
pub use status_bar::{draw_status_bar, MIN_GAP_PX};
pub use tab_bar::{
    draw_tab_bar, draw_tab_bar_icons, draw_tab_bar_icons_with_chrome, draw_tab_bar_with_chrome,
};
pub use text_display::{draw_text_display, gtk_text_display_layout};
pub use text_input::{draw_text_input, gtk_text_input_layout};
pub use toast::gtk_toast_stack_layout;
pub use toolbar::{draw_toolbar, draw_toolbar_with_options, gtk_toolbar_layout};
pub use tooltip::{draw_tooltip, draw_tooltip_with_chrome};
pub use tree::{draw_tree, gtk_tree_layout};

/// Convert a `quadraui::Color` (0-255 RGBA) into Cairo's normalised
/// `(r, g, b)` tuple. Alpha is dropped — Cairo supports
/// `set_source_rgba` if a future primitive needs it.
pub fn cairo_rgb(c: Color) -> (f64, f64, f64) {
    (c.r as f64 / 255.0, c.g as f64 / 255.0, c.b as f64 / 255.0)
}

/// `set_source_rgb` shortcut used internally by the rasterisers and
/// available to apps that want their auxiliary draws to colour-match.
pub fn set_source(cr: &Context, c: Color) {
    let (r, g, b) = cairo_rgb(c);
    cr.set_source_rgb(r, g, b);
}

/// `set_source_rgba` shortcut that honours `c.a`, unlike [`set_source`]
/// (see [`cairo_rgb`]'s doc for why that one drops it).
///
/// Used by [`crate::gtk::backend::GtkBackend::surface_fill_rect`]
/// (issue #811, `PaintSurface` Phase 2d) so a translucent fill —
/// `primitives::scrollbar::paint`'s overlay track/thumb, most notably —
/// blends against whatever is already on the target instead of
/// silently rendering opaque. Before this, `GtkBackend`'s own
/// `PaintSurface::surface_fill_rect` was the *one* of the three pixel
/// backends whose fill didn't honour alpha (macOS's `ns_fill_rect` and
/// Windows's `win::text::fill_rect` both already paint a real
/// translucent brush/fill — see quadraui#791, which fixed Windows's
/// side of this same gap for the pre-`PaintSurface`
/// `win::scrollbar::draw_scrollbar`). Every existing
/// `surface_fill_rect` caller (`primitives::{chart,form,terminal,
/// text_display}`) always passes an opaque (`a: 255`) `Color`, so
/// switching the GTK impl from `set_source_rgb` to this makes no visual
/// difference to any of them — it only starts honouring alpha for
/// colours that actually carry it.
pub fn set_source_rgba(cr: &Context, c: Color) {
    cr.set_source_rgba(
        c.r as f64 / 255.0,
        c.g as f64 / 255.0,
        c.b as f64 / 255.0,
        c.a as f64 / 255.0,
    );
}

/// Build a closed rounded-rectangle Cairo path with corner radius `r`.
/// Used by rasterisers that need bordered/clipped rounded rects
/// (context menu, list view, command center).
pub(crate) fn rounded_rect_path(cr: &Context, x: f64, y: f64, w: f64, h: f64, r: f64) {
    use std::f64::consts::{FRAC_PI_2, PI};
    cr.new_path();
    cr.arc(x + w - r, y + r, r, -FRAC_PI_2, 0.0);
    cr.arc(x + w - r, y + h - r, r, 0.0, FRAC_PI_2);
    cr.arc(x + r, y + h - r, r, FRAC_PI_2, PI);
    cr.arc(x + r, y + r, r, PI, 3.0 * FRAC_PI_2);
    cr.close_path();
}

/// Re-export so apps can name the Pango layout type without depending
/// on `gtk4::pango` directly.
pub use pango::Layout as PangoLayout;

/// Default font family GTK chrome text falls back to for Nerd-Font
/// glyphs, and the value [`current_nerd_font_fallback_family`] returns
/// until [`Backend::set_nerd_font_fallback`][crate::Backend::set_nerd_font_fallback]
/// overrides it.
///
/// Matches the family [`activity_bar::ICON_FONT_DESC`] and
/// [`tab_bar::tab_icon_font`] already pin for the activity bar and tab
/// icons (#620) — kept as one named constant so retargeting to a
/// differently-named Nerd Font patch (or a consumer's fontconfig alias)
/// is a one-line change instead of hunting every rasteriser that
/// references the literal.
pub const NERD_FONT_FALLBACK_FAMILY: &str = "Symbols Nerd Font";

// ── Runtime-settable fallback family (issue #929) ───────────────────────
//
// `chrome_font_description`/`with_nerd_font_fallback`/`tab_icon_font`
// used to bake in `NERD_FONT_FALLBACK_FAMILY` directly. GTK's own
// glyph resolution already worked without any of this — fontconfig
// cascades to a system-installed Nerd Font on its own — but #929 asks
// for `Backend::set_nerd_font_fallback` to be the *portable* entry
// point on every backend, GTK included, so an app that targets the
// trait method rather than this module's GTK-specific constant gets
// the same effect everywhere. A `thread_local` (not a `GtkBackend`
// field) because the constant it replaces was itself free-standing,
// consulted by plain functions (`tab_icon_font`, and — via
// `chrome_font_description` — dozens of `GtkBackend` rasterisers) with
// no `&self` in scope; threading an instance field through every one
// of those call sites for a value that is, in practice, one process's
// one fontconfig-visible font family would have been a much larger
// diff for no behavioural difference. GTK is single-threaded (the glib
// main loop owns rendering), so a plain `RefCell` is enough — no
// `Mutex` needed.
thread_local! {
    static NERD_FONT_FALLBACK: std::cell::RefCell<String> =
        std::cell::RefCell::new(NERD_FONT_FALLBACK_FAMILY.to_string());
}

/// The family [`with_nerd_font_fallback`]/[`chrome_font_description`]/
/// [`tab_bar::tab_icon_font`] currently append/substitute for Nerd-Font
/// glyphs — [`NERD_FONT_FALLBACK_FAMILY`] until
/// [`set_current_nerd_font_fallback_family`] overrides it.
pub(crate) fn current_nerd_font_fallback_family() -> String {
    NERD_FONT_FALLBACK.with(|f| f.borrow().clone())
}

/// Override the family every subsequent call on this thread to
/// [`with_nerd_font_fallback`]/[`chrome_font_description`]/
/// [`tab_bar::tab_icon_font`] resolves against — thread-wide, not
/// process-wide, since the backing store is a `thread_local` (see its
/// doc above); GTK's single-threaded glib main loop is what makes that
/// equivalent to "process-wide" in practice for a real app. Backs
/// `GtkBackend::set_nerd_font_fallback` (issue #929).
pub(crate) fn set_current_nerd_font_fallback_family(family: &str) {
    NERD_FONT_FALLBACK.with(|f| *f.borrow_mut() = family.to_string());
}

/// Build a Pango font description for GTK chrome text (list/tree/
/// palette rows, status bar segments, menu items, dialogs, ...) from a
/// `ui_font`-style description string (e.g. `"Sans 11"`), appending
/// [`NERD_FONT_FALLBACK_FAMILY`] to the family list.
///
/// Chrome primitives paint icon glyphs (`Icon::glyph`) and — on the
/// status bar — consumer-composed segment text that can itself embed a
/// raw Nerd-Font codepoint, inline with ordinary label text, all using
/// the *same* Pango layout and font. Most system UI fonts ("Sans",
/// "Cantarell", ...) have no coverage for the Private-Use-Area
/// codepoints Nerd Font glyphs live in, so without an explicit fallback
/// family in the description Pango's per-character font substitution is
/// left to guess at a system font that happens to cover that codepoint
/// range — unreliable, and prone to picking an unrelated font that
/// defines *something* there, rather than the intended glyph. Naming
/// the fallback family explicitly, in family-list order right after the
/// caller's own family, makes glyph resolution deterministic instead of
/// dependent on what else is installed (quadraui#416).
///
/// Harmless when no glyph is being painted: Pango only consults a later
/// family in the list for characters the earlier one doesn't cover, so
/// plain text keeps rendering in the caller's own `ui_font` family.
pub(crate) fn chrome_font_description(ui_font: &str) -> pango::FontDescription {
    with_nerd_font_fallback(&pango::FontDescription::from_string(ui_font))
}

/// Clone `base` with [`NERD_FONT_FALLBACK_FAMILY`] (or whatever the app
/// last set via [`crate::Backend::set_nerd_font_fallback`]) and
/// [`crate::codicon::FONT_FAMILY`] appended to its family list. Used
/// where a rasteriser can't build a fresh description from a `ui_font`
/// string (the caller only hands it an already-live
/// [`pango::FontDescription`] to paint one glyph with, e.g. an icon
/// inside an otherwise editor-font-painted row) — see
/// [`chrome_font_description`] for the string-based sibling and the full
/// rationale.
///
/// The codicon family is appended *unconditionally* —
/// every built-in chrome glyph this crate paints through this function
/// (tree chevrons, tab dirty/close, the context-menu submenu arrow)
/// must resolve regardless of whatever the app's own `set_nerd_font_fallback`
/// call last chose, and `codicon::FONT_FAMILY`'s own doc explains why
/// that can never shadow an app-supplied icon glyph: `FONT_FAMILY` is
/// always appended *last*, after `app_fallback_family`, so an
/// app-supplied font earlier in the list wins any overlapping
/// codepoint.
pub(crate) fn with_nerd_font_fallback(base: &pango::FontDescription) -> pango::FontDescription {
    let mut desc = base.clone();
    let fallback = current_nerd_font_fallback_family();
    let family = desc.family().map(|f| f.to_string()).unwrap_or_default();
    let codicon = crate::codicon::FONT_FAMILY;
    let with_fallback = if family.is_empty() {
        format!("{fallback},{codicon}")
    } else {
        format!("{family},{fallback},{codicon}")
    };
    desc.set_family(&with_fallback);
    desc
}

#[cfg(test)]
mod tests {
    use super::*;

    /// The family list must lead with the caller's own `ui_font` family
    /// (so ordinary chrome text keeps rendering in the configured font)
    /// and include [`NERD_FONT_FALLBACK_FAMILY`] afterward (so a glyph
    /// the primary family can't cover — e.g. a Nerd-Font icon — still
    /// resolves instead of rendering tofu/blank).
    #[test]
    fn chrome_font_description_appends_nerd_font_fallback() {
        let desc = chrome_font_description("Sans 11");
        let family = desc.family().expect("family set").to_string();
        assert_eq!(family, "Sans,Symbols Nerd Font,codicon");
    }

    /// The point size from the input description string must survive —
    /// only the family list is touched.
    #[test]
    fn chrome_font_description_preserves_size() {
        let plain = pango::FontDescription::from_string("Sans 11");
        let desc = chrome_font_description("Sans 11");
        assert_eq!(desc.size(), plain.size());
    }

    /// A caller-supplied multi-family list (a consumer's own fallback
    /// chain) is preserved verbatim, with the Nerd Font family appended
    /// rather than replacing it.
    #[test]
    fn chrome_font_description_appends_after_existing_family_list() {
        let desc = chrome_font_description("Cantarell,DejaVu Sans 12");
        let family = desc.family().expect("family set").to_string();
        assert_eq!(family, "Cantarell,DejaVu Sans,Symbols Nerd Font,codicon");
    }

    /// [`with_nerd_font_fallback`] mirrors [`chrome_font_description`]
    /// for callers that only have a live `FontDescription` to hand (e.g.
    /// `gtk::palette::draw_palette`'s icon-glyph swap, which must not
    /// disturb the row's base font for the label text painted around
    /// it) — same family-list-append behaviour, size preserved.
    #[test]
    fn with_nerd_font_fallback_appends_to_an_existing_description() {
        let base = pango::FontDescription::from_string("Monospace 13");
        let desc = with_nerd_font_fallback(&base);
        let family = desc.family().expect("family set").to_string();
        assert_eq!(family, "Monospace,Symbols Nerd Font,codicon");
        assert_eq!(desc.size(), base.size());
    }

    /// Resets the thread-local fallback family to
    /// [`NERD_FONT_FALLBACK_FAMILY`] on drop, including on an early
    /// return/panic — so a test that calls
    /// [`set_current_nerd_font_fallback_family`] can't leak its override
    /// into a later test that happens to land on the same worker thread
    /// (`cargo test`'s harness reuses OS threads across `#[test]`
    /// functions, and this module's override lives in a `thread_local`
    /// — see that function's doc for why it isn't a per-instance field).
    struct FallbackFamilyGuard;
    impl Drop for FallbackFamilyGuard {
        fn drop(&mut self) {
            set_current_nerd_font_fallback_family(NERD_FONT_FALLBACK_FAMILY);
        }
    }

    /// [`set_current_nerd_font_fallback_family`] is what
    /// `GtkBackend::set_nerd_font_fallback` calls — this test exercises
    /// the module-level primitive directly (a `GtkBackend` needs a live
    /// GTK application to construct) and confirms every reader
    /// (`with_nerd_font_fallback`, and therefore
    /// `chrome_font_description`) picks up the override.
    #[test]
    fn set_current_nerd_font_fallback_family_is_read_back_by_with_nerd_font_fallback() {
        let _guard = FallbackFamilyGuard;
        assert_eq!(
            current_nerd_font_fallback_family(),
            NERD_FONT_FALLBACK_FAMILY
        );

        set_current_nerd_font_fallback_family("Consumer Icons");
        assert_eq!(current_nerd_font_fallback_family(), "Consumer Icons");

        let desc = chrome_font_description("Sans 11");
        let family = desc.family().expect("family set").to_string();
        assert_eq!(family, "Sans,Consumer Icons,codicon");
    }

    /// [`tab_bar::tab_icon_font`] replaces the family entirely (see its
    /// own doc for why) rather than appending — confirm it also tracks
    /// the override, not just `with_nerd_font_fallback`'s append path.
    #[test]
    fn tab_icon_font_tracks_the_overridden_fallback_family() {
        let _guard = FallbackFamilyGuard;
        set_current_nerd_font_fallback_family("Consumer Icons");

        let base = pango::FontDescription::from_string("Sans 11");
        let desc = tab_bar::tab_icon_font(&base);
        let family = desc.family().expect("family set").to_string();
        assert_eq!(family, "Consumer Icons, codicon, monospace");
    }
}