uzor 1.5.1

Core UI engine — geometry, interaction, input state
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
412
413
414
415
416
417
418
419
420
421
422
423
//! Fluent builder for constructing an uzor app.

use crate::layout::docking::DockPanel;

use super::app::{App, AppConfig, NoPanel};
use super::multi_window::{WindowSpec, WindowKey};

// RgbaIcon, RenderBackend, and CornerStyle canonical definitions live in uzor::platform::types.
// Re-exported here so existing callers of `uzor::framework::builder::{RgbaIcon,RenderBackend}`
// keep working without changes.
pub use crate::platform::types::{CornerStyle, RgbaIcon, RenderBackend, RenderFamily};

// ── AnyFactory ───────────────────────────────────────────────────────────────
//
// The factory is stored as `Box<dyn AnyFactory>` in `BuiltApp` so that
// platform crates (e.g. `uzor-desktop`) can downcast it back to the concrete
// type (e.g. `uzor_render_hub::VelloGpuSurfaceFactory`) and call the actual
// surface-creation methods without `uzor` needing to depend on `uzor-render-hub`.

/// Opaque factory wrapper stored in [`BuiltApp`].
///
/// Platform crates downcast this to their concrete factory type via
/// [`AnyFactory::into_any`] followed by `downcast::<ConcreteFactory>()`.
pub trait AnyFactory: Send + Sync + 'static {
    fn into_any(self: Box<Self>) -> Box<dyn std::any::Any + Send + Sync>;
}

/// Convenience alias used in [`AppBuilder::surface_factory`].
///
/// Any type that is `Send + Sync + 'static` can be wrapped in the builder.
/// Platform crates (e.g. `uzor-desktop`) recover the concrete type via
/// `built.factory.unwrap().into_any().downcast::<T>()`.
pub type RenderSurfaceFactory = dyn AnyFactory;

/// Blanket implementation — every `Send + Sync + 'static` type is an
/// [`AnyFactory`] automatically.
impl<T: Send + Sync + 'static> AnyFactory for T {
    fn into_any(self: Box<Self>) -> Box<dyn std::any::Any + Send + Sync> {
        self
    }
}

// ── BuildError ────────────────────────────────────────────────────────────────

/// Errors that can occur when calling [`AppBuilder::build`] or icon helpers.
#[derive(Debug)]
pub enum BuildError {
    /// PNG icon bytes could not be decoded or converted to RGBA8.
    IconDecode(String),
}

impl std::fmt::Display for BuildError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            BuildError::IconDecode(msg) => {
                write!(f, "icon PNG decode failed: {msg}")
            }
        }
    }
}

impl std::error::Error for BuildError {}

// ── TraySpec ──────────────────────────────────────────────────────────────────

/// Spec for a system-tray icon spawned automatically by the builder.
pub struct TraySpec {
    pub tooltip: Option<String>,
    pub items:   Vec<(String, String, bool)>, // (id, label, enabled)
}

// ── BuiltApp ──────────────────────────────────────────────────────────────────

/// The result of [`AppBuilder::build`].
///
/// Bundles all data needed to start the runtime.  Consumed by `uzor-desktop`
/// (or another platform crate) to create the event loop, window(s), and GPU
/// pipeline.
///
/// Fields are `pub` so that external platform crates (e.g. `uzor-desktop`)
/// can destructure them without reflection.
pub struct BuiltApp<A: App<P>, P: DockPanel> {
    #[doc(hidden)]
    pub app:     A,
    #[doc(hidden)]
    pub config:  AppConfig,
    /// `None` means "let the platform runtime autodetect".
    #[doc(hidden)]
    pub backend: Option<RenderBackend>,
    /// `None` means "the platform runtime resolves the family from
    /// `UZOR_RENDER_FAMILY` / [`RenderFamily::default`]" — ignored
    /// entirely when `backend` is `Some` (an explicit backend always
    /// skips family resolution, owner decision 2026-07-24).
    #[doc(hidden)]
    pub render_family: Option<RenderFamily>,
    #[doc(hidden)]
    pub factory: Option<Box<dyn AnyFactory>>,
    #[doc(hidden)]
    pub tray:    Option<TraySpec>,
    #[doc(hidden)]
    pub windows: Vec<WindowSpec>,
    #[doc(hidden)]
    pub _phantom: std::marker::PhantomData<P>,
}

// ── AppBuilder ────────────────────────────────────────────────────────────────

/// Fluent builder for configuring an uzor app.
///
/// # Generic parameters
///
/// - `A` — the app struct that implements [`App<P>`].
/// - `P` — the dock-panel type.  Defaults to [`NoPanel`].
pub struct AppBuilder<A, P = NoPanel>
where
    A: App<P>,
    P: DockPanel,
{
    app: A,
    config: AppConfig,
    backend: Option<RenderBackend>,
    render_family: Option<RenderFamily>,
    factory: Option<Box<dyn AnyFactory>>,
    tray: Option<TraySpec>,
    windows: Vec<WindowSpec>,
    _phantom: std::marker::PhantomData<P>,
}

impl<A, P> AppBuilder<A, P>
where
    A: App<P>,
    P: DockPanel + Default + Clone + 'static,
{
    /// Create a builder wrapping `app`.
    pub fn new(app: A) -> Self {
        Self {
            app,
            config: AppConfig::default(),
            backend: None,
            render_family: None,
            factory: None,
            tray: None,
            windows: Vec::new(),
            _phantom: std::marker::PhantomData,
        }
    }

    /// Queue a window for the manager to create at startup.
    pub fn window(mut self, spec: WindowSpec) -> Self {
        self.windows.push(spec);
        self
    }

    /// Spawn a system-tray icon when the runtime starts.
    pub fn tray(mut self, tooltip: impl Into<String>) -> Self {
        self.tray = Some(TraySpec {
            tooltip: Some(tooltip.into()),
            items:   Vec::new(),
        });
        self
    }

    /// Add a tray-menu item.  Requires `.tray(tooltip)` called first.
    pub fn tray_item(mut self, id: impl Into<String>, label: impl Into<String>) -> Self {
        if let Some(ref mut t) = self.tray {
            t.items.push((id.into(), label.into(), true));
        }
        self
    }

    /// Add a disabled (greyed-out) tray-menu item.  Requires `.tray` first.
    pub fn tray_item_disabled(mut self, id: impl Into<String>, label: impl Into<String>) -> Self {
        if let Some(ref mut t) = self.tray {
            t.items.push((id.into(), label.into(), false));
        }
        self
    }

    // ── Configuration setters ─────────────────────────────────────────────────

    /// Replace the entire [`AppConfig`] at once.
    pub fn config(mut self, config: AppConfig) -> Self {
        self.config = config;
        self
    }

    /// Set the window title.
    pub fn title(mut self, t: impl Into<String>) -> Self {
        self.config.title = t.into();
        self
    }

    /// Set the initial logical window size.
    pub fn size(mut self, w: u32, h: u32) -> Self {
        self.config.initial_size = (w, h);
        self
    }

    /// Set the minimum logical window size.
    pub fn min_size(mut self, min: Option<(u32, u32)>) -> Self {
        self.config.min_size = min;
        self
    }

    /// Enable or disable OS-native window decorations.
    pub fn decorations(mut self, on: bool) -> Self {
        self.config.decorations = on;
        self
    }

    /// Enable or disable multi-window support.
    pub fn multi_window(mut self, on: bool) -> Self {
        self.config.multi_window = on;
        self
    }

    /// Set the FPS limit (`0` = unlimited).
    pub fn fps_limit(mut self, fps: u32) -> Self {
        self.config.fps_limit = fps;
        self
    }

    /// Set the MSAA sample count.
    pub fn msaa(mut self, samples: u8) -> Self {
        self.config.msaa_samples = samples;
        self
    }

    /// Enable or disable VSync.
    pub fn vsync(mut self, on: bool) -> Self {
        self.config.vsync = on;
        self
    }

    /// Enable the local agent control-plane HTTP server on
    /// `127.0.0.1:port`.  External agents (LLMs, QA tooling, scripts)
    /// read live LM state and inject input through it.  See
    /// `uzor-agent-api` for the route catalogue.
    pub fn agent_api(mut self, port: u16) -> Self {
        self.config.agent_api_port = Some(port);
        self
    }

    /// Default baseline repaint cadence for every window that doesn't
    /// override it via [`WindowSpec::tick_rate`].  `Capped(60)` by
    /// default.  Use [`crate::render::TickRate::Dirty`] for the
    /// legacy event-driven path.
    pub fn default_tick_rate(mut self, rate: crate::render::TickRate) -> Self {
        self.config.default_tick_rate = rate;
        self
    }

    /// Set the clear colour as `0xAARRGGBB`.
    pub fn background(mut self, argb: u32) -> Self {
        self.config.background = argb;
        self
    }

    /// Enforce single-instance via a Win32 named mutex.
    pub fn single_instance(mut self, name: Option<impl Into<String>>) -> Self {
        self.config.single_instance = name.map(Into::into);
        self
    }

    /// Set the app-level border accent colour (`0x00RRGGBB` ARGB). `None` = OS default.
    ///
    /// Per-window [`WindowSpec::border_color`] overrides this value.
    pub fn border_color(mut self, color: Option<u32>) -> Self {
        self.config.border_color = color;
        self
    }

    /// Set the app-level corner-rounding preference.
    ///
    /// Per-window [`WindowSpec::corner_style`] overrides this value.
    pub fn corner_style(mut self, style: CornerStyle) -> Self {
        self.config.corner_style = style;
        self
    }

    /// Set the app-level drop-shadow override. `None` = OS default.
    ///
    /// Per-window [`WindowSpec::shadow`] overrides this value.
    pub fn shadow(mut self, on: bool) -> Self {
        self.config.shadow = Some(on);
        self
    }

    /// Set the window icon from a pre-built [`RgbaIcon`].
    pub fn icon(mut self, icon: RgbaIcon) -> Self {
        self.config.icon = Some(icon);
        self
    }

    /// Set the window icon by decoding a PNG byte slice.
    ///
    /// # Errors
    ///
    /// Returns `Err(BuildError::IconDecode)` if the bytes are not valid PNG.
    pub fn icon_from_png(mut self, png_bytes: &[u8]) -> Result<Self, BuildError> {
        let icon = decode_png_to_rgba(png_bytes)
            .map_err(|e| BuildError::IconDecode(e))?;
        self.config.icon = Some(icon);
        Ok(self)
    }

    // ── Infrastructure setters ────────────────────────────────────────────────

    /// Select the rendering backend (override — skips autodetect entirely,
    /// including [`Self::render_family`] resolution).
    ///
    /// When omitted, `uzor-desktop` will call `RenderHub::autodetect()` at
    /// startup and pick the best available backend automatically, for
    /// whichever [`RenderFamily`] [`Self::render_family`] (or the
    /// `UZOR_RENDER_FAMILY` env var, or [`RenderFamily::default`]) resolves
    /// to.
    pub fn backend(mut self, backend: RenderBackend) -> Self {
        self.backend = Some(backend);
        self
    }

    /// Select the coarse render family autodetect should target — `Vello`
    /// (today's default) or `Urx`. **Ignored when [`Self::backend`] is also
    /// called** — an explicit backend always wins outright.
    ///
    /// Owner decision 2026-07-24: no default flip, ever. Both families stay
    /// first-class; this is the per-app knob that picks between them when
    /// no explicit backend is set. Full precedence: explicit `.backend(...)`
    /// > `UZOR_RENDER_FAMILY` env var (case-insensitive `vello`/`urx`) >
    /// this setting > [`RenderFamily::default`] (`Vello`). 3D is always
    /// URX regardless of this flag — it has no effect on the 3D dispatch
    /// path.
    pub fn render_family(mut self, family: RenderFamily) -> Self {
        self.render_family = Some(family);
        self
    }

    /// Supply a surface factory (override — skips the hub's built-in factory).
    ///
    /// Pass any concrete factory (e.g. `VelloGpuSurfaceFactory`) boxed as
    /// `Box<T>` where `T: Send + Sync + 'static`.  Platform crates (e.g.
    /// `uzor-desktop`) recover the concrete type via
    /// `built.factory.unwrap().into_any().downcast::<T>()`.
    pub fn surface_factory<T: Send + Sync + 'static>(mut self, factory: Box<T>) -> Self {
        self.factory = Some(factory as Box<dyn AnyFactory>);
        self
    }

    // ── Terminal method ───────────────────────────────────────────────────────

    /// Consume the builder and produce a [`BuiltApp`] ready for a platform
    /// runtime (e.g. `uzor-desktop`) to consume.
    ///
    /// When neither `.backend(...)` nor `.surface_factory(...)` was called, the
    /// platform runtime (e.g. `uzor-desktop`) will call
    /// `RenderHub::autodetect()` automatically — no explicit backend selection
    /// is required.
    pub fn build(mut self) -> Result<BuiltApp<A, P>, BuildError> {
        // Synthesise a default window spec from AppConfig if the caller
        // didn't queue any explicit windows.
        if self.windows.is_empty() {
            let default = WindowSpec::new(
                WindowKey::new("main"),
                if self.config.title.is_empty() { "uzor".to_string() }
                else { self.config.title.clone() },
            )
            .size(self.config.initial_size.0, self.config.initial_size.1)
            .decorations(self.config.decorations)
            .background(self.config.background);
            let default = if let Some((mw, mh)) = self.config.min_size {
                default.min_size(mw, mh)
            } else {
                default
            };
            self.windows.push(default);
        }

        Ok(BuiltApp {
            app:     self.app,
            config:  self.config,
            backend: self.backend,
            render_family: self.render_family,
            factory: self.factory,
            tray:    self.tray,
            windows: self.windows,
            _phantom: std::marker::PhantomData,
        })
    }
}

// ── PNG decode helper (no uzor-render-hub dep) ────────────────────────────────

#[cfg(feature = "framework-png")]
fn decode_png_to_rgba(png_bytes: &[u8]) -> Result<RgbaIcon, String> {
    use image::ImageDecoder;
    use std::io::Cursor;

    let decoder = image::codecs::png::PngDecoder::new(Cursor::new(png_bytes))
        .map_err(|e| e.to_string())?;

    let (width, height) = decoder.dimensions();
    let total_bytes = decoder.total_bytes() as usize;

    let mut raw = vec![0u8; total_bytes];
    decoder
        .read_image(&mut raw)
        .map_err(|e| e.to_string())?;

    let rgba: Vec<u8> = if total_bytes == (width * height * 4) as usize {
        raw
    } else {
        let img = image::load_from_memory_with_format(png_bytes, image::ImageFormat::Png)
            .map_err(|e| e.to_string())?;
        img.into_rgba8().into_raw()
    };

    Ok(RgbaIcon::from_rgba(width, height, rgba))
}

#[cfg(not(feature = "framework-png"))]
fn decode_png_to_rgba(_png_bytes: &[u8]) -> Result<RgbaIcon, String> {
    Err("PNG icon decoding requires the 'framework-png' feature".to_string())
}