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
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
//! Stable application-authoring API for the ZSUI 0.2 release line.
//!
//! Items in this module follow the compatibility policy documented in
//! `docs/api-stability.md`: patch releases in the 0.2 line may add APIs, but
//! do not remove or change existing signatures or serialized meanings.
//!
//! ```no_run
//! # #[cfg(all(feature = "button", feature = "label"))]
//! # fn main() -> Result<(), zsui::stable::Error> {
//! use zsui::stable::{button, column, text, window, Dp, Element, UpdateContext};
//!
//! #[derive(Clone)]
//! enum Message {
//! Increment,
//! }
//!
//! struct State {
//! count: u32,
//! }
//!
//! fn view(state: &State) -> Element<Message> {
//! column([
//! text(format!("Count: {}", state.count)),
//! button("Increment").on_click(Message::Increment),
//! ])
//! .gap(Dp::new(12.0))
//! .padding(Dp::new(20.0))
//! }
//!
//! fn update(state: &mut State, message: Message, _cx: &mut UpdateContext<'_>) {
//! match message {
//! Message::Increment => state.count += 1,
//! }
//! }
//!
//! window("Stable ZSUI")
//! .size(480, 320)
//! .stateful(State { count: 0 }, view, update)
//! .run()?;
//! # Ok(())
//! # }
//! # #[cfg(not(all(feature = "button", feature = "label")))]
//! # fn main() {}
//! ```
#![deny(missing_docs)]
use std::{fmt, path::PathBuf};
#[path = "stable_controls.rs"]
mod controls;
pub use controls::*;
pub use crate::{
DialogButtons, DialogLevel, DialogResponse, DirectoryDialogSpec, FileDialogFilter,
FileDialogSpec, NativeDialogSpec, SaveFileDialogSpec,
};
/// A platform-independent logical length measured at 96 DPI.
#[derive(Debug, Clone, Copy, Default, PartialEq, PartialOrd)]
pub struct Dp(f32);
impl Dp {
/// Creates a logical length from its scalar value.
pub const fn new(value: f32) -> Self {
Self(value)
}
/// Returns the scalar logical value.
pub const fn get(self) -> f32 {
self.0
}
}
impl From<Dp> for crate::Dp {
fn from(value: Dp) -> Self {
Self::new(value.0)
}
}
/// Distribution of children along a row or column's main axis.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum Justify {
/// Places children at the start of the axis.
#[default]
Start,
/// Centers children as one group.
Center,
/// Places children at the end of the axis.
End,
/// Distributes free space between children.
SpaceBetween,
/// Distributes free space around children.
SpaceAround,
/// Distributes equal free space before, between and after children.
SpaceEvenly,
}
impl From<Justify> for crate::ViewJustify {
fn from(value: Justify) -> Self {
match value {
Justify::Start => Self::Start,
Justify::Center => Self::Center,
Justify::End => Self::End,
Justify::SpaceBetween => Self::SpaceBetween,
Justify::SpaceAround => Self::SpaceAround,
Justify::SpaceEvenly => Self::SpaceEvenly,
}
}
}
/// Placement of children along a row or column's cross axis.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum Align {
/// Uses native intrinsic sizing and platform-appropriate placement.
#[default]
Auto,
/// Places children at the start of the cross axis.
Start,
/// Centers children on the cross axis.
Center,
/// Places children at the end of the cross axis.
End,
/// Expands children without a fixed cross-axis size to fill the axis.
Stretch,
}
impl From<Align> for crate::ViewAlign {
fn from(value: Align) -> Self {
match value {
Align::Auto => Self::Auto,
Align::Start => Self::Start,
Align::Center => Self::Center,
Align::End => Self::End,
Align::Stretch => Self::Stretch,
}
}
}
/// A deterministic application-level widget identity.
///
/// IDs are optional because ZSUI assigns structural IDs to interactive nodes.
/// Supply one when state, automation or another component must address the
/// same widget across tree insertion or reordering.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct WidgetId(u64);
impl WidgetId {
/// Creates an explicit widget identity.
pub const fn new(value: u64) -> Self {
Self(value)
}
/// Returns the numeric identity chosen by the application.
pub const fn get(self) -> u64 {
self.0
}
}
impl From<WidgetId> for crate::WidgetId {
fn from(value: WidgetId) -> Self {
Self::new(value.0)
}
}
/// A cloneable, thread-safe request handle for rebuilding a stateful window.
///
/// This is intended for worker-thread results, streaming responses and other
/// app-owned state changes that must become visible without a polling widget.
/// Requests are coalesced and the rebuild always runs on the native UI thread.
/// Attach each handle to exactly one window created with
/// [`WindowBuilder::stateful`].
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct InvalidationHandle {
inner: crate::UiInvalidationHandle,
}
impl InvalidationHandle {
/// Creates an invalidation handle that can be attached to a window.
pub fn new() -> Self {
Self::default()
}
/// Requests one stateful View rebuild and repaint.
///
/// Returns `true` when a new request was queued, or `false` when an
/// earlier request is already waiting for the native UI thread.
pub fn request_rebuild(&self) -> bool {
self.inner.request_rebuild()
}
}
/// An opaque retained UI element that carries messages of type `Message`.
///
/// The platform-specific render, input and accessibility objects remain
/// private. Applications compose elements and return them from one shared
/// `view` function on every desktop platform.
#[derive(Debug, Clone)]
pub struct Element<Message> {
inner: crate::ViewNode<Message>,
}
impl<Message> Element<Message> {
fn from_raw(inner: crate::ViewNode<Message>) -> Self {
Self { inner }
}
fn into_raw(self) -> crate::ViewNode<Message> {
self.inner
}
/// Assigns an explicit identity to the element.
pub fn id(mut self, id: WidgetId) -> Self {
self.inner = self.inner.id(id.into());
self
}
/// Sets uniform inner padding.
pub fn padding(mut self, padding: Dp) -> Self {
self.inner = self.inner.padding(padding.into());
self
}
/// Sets the spacing between children in a row or column.
pub fn gap(mut self, gap: Dp) -> Self {
self.inner = self.inner.gap(gap.into());
self
}
/// Sets a fixed logical width.
pub fn width(mut self, width: Dp) -> Self {
self.inner = self.inner.width(width.into());
self
}
/// Sets a fixed logical height.
pub fn height(mut self, height: Dp) -> Self {
self.inner = self.inner.height(height.into());
self
}
/// Sets the minimum logical width used by intrinsic layout.
pub fn min_width(mut self, width: Dp) -> Self {
self.inner = self.inner.min_width(width.into());
self
}
/// Sets the minimum logical height used by intrinsic layout.
pub fn min_height(mut self, height: Dp) -> Self {
self.inner = self.inner.min_height(height.into());
self
}
/// Sets the non-negative share of remaining space assigned by its parent.
pub fn flex(mut self, factor: f32) -> Self {
self.inner = self.inner.flex(factor);
self
}
/// Distributes this container's children along its main axis.
pub fn justify(mut self, justify: Justify) -> Self {
self.inner = self.inner.justify(justify.into());
self
}
/// Places this container's children along its cross axis.
pub fn align(mut self, align: Align) -> Self {
self.inner = self.inner.align(align.into());
self
}
/// Enables vertical scrolling when width-aware intrinsic content exceeds
/// this element's viewport.
#[cfg(feature = "scroll")]
pub fn auto_scroll_y(mut self) -> Self {
self.inner = self.inner.auto_scroll_y();
self
}
/// Sets a semantic accessibility label without exposing platform APIs.
#[cfg(feature = "accessibility")]
pub fn accessibility_label(mut self, label: impl Into<String>) -> Self {
self.inner = self.inner.accessibility_label(label);
self
}
/// Attaches a native-style tooltip to the element.
#[cfg(feature = "tooltip")]
pub fn tooltip(mut self, label: impl Into<String>) -> Self {
self.inner = self.inner.tooltip(label);
self
}
}
impl<Message: Clone> Element<Message> {
/// Emits `message` when a button or clickable canvas is activated.
#[cfg(any(feature = "button", feature = "canvas"))]
pub fn on_click(mut self, message: Message) -> Self {
self.inner = self.inner.on_click(message);
self
}
/// Emits a mapped message when editable text changes.
#[cfg(feature = "textbox")]
pub fn on_change(mut self, map: fn(String) -> Message) -> Self {
self.inner = self.inner.on_change(map);
self
}
/// Emits a mapped message when a toggle changes value.
#[cfg(feature = "toggle")]
pub fn on_toggle(mut self, map: fn(bool) -> Message) -> Self {
self.inner = self.inner.on_toggle(map);
self
}
/// Controls whether a button can receive focus and activation.
#[cfg(feature = "button")]
pub fn enabled(mut self, enabled: bool) -> Self {
self.inner = self.inner.enabled(enabled);
self
}
}
/// Creates a vertical stack from child elements.
pub fn column<Message>(children: impl IntoIterator<Item = Element<Message>>) -> Element<Message> {
Element::from_raw(crate::column(children.into_iter().map(Element::into_raw)))
}
/// Creates a horizontal stack from child elements.
pub fn row<Message>(children: impl IntoIterator<Item = Element<Message>>) -> Element<Message> {
Element::from_raw(crate::row(children.into_iter().map(Element::into_raw)))
}
/// Creates a flexible empty layout element.
pub fn spacer<Message>() -> Element<Message> {
Element::from_raw(crate::spacer())
}
/// Creates body text resolved through the current platform's native type ramp.
#[cfg(feature = "label")]
pub fn text<Message>(value: impl Into<String>) -> Element<Message> {
Element::from_raw(crate::text(value))
}
/// Creates a native-style push button.
#[cfg(feature = "button")]
pub fn button<Message>(label: impl Into<String>) -> Element<Message> {
Element::from_raw(crate::button(label))
}
/// Creates a single-line editable text field.
#[cfg(feature = "textbox")]
pub fn textbox<Message>(value: impl Into<String>) -> Element<Message> {
Element::from_raw(crate::textbox(value))
}
/// Creates a platform-native toggle surface.
#[cfg(feature = "toggle")]
pub fn toggle<Message>(checked: bool) -> Element<Message> {
Element::from_raw(crate::toggle(checked))
}
/// A cancellation token shared by one native effect and all its clones.
pub type EffectCancellation = crate::AppEffectCancellation;
/// A request rejected before admission to a bounded application queue.
pub type QueueFull = crate::QueueFull;
/// Mutable commands available while reducing a typed application message.
pub struct UpdateContext<'a> {
inner: &'a mut crate::AppCx,
}
impl UpdateContext<'_> {
/// Requests orderly shutdown after the current update completes.
pub fn quit(&mut self) {
self.inner.quit();
}
/// Queues a native service or returns backpressure; the token cancels pending delivery.
pub fn try_open_file_dialog<Message: Send + 'static>(
&mut self,
spec: FileDialogSpec,
respond: impl Fn(Result<Option<Vec<PathBuf>>, Error>) -> Message + Send + Sync + 'static,
) -> Result<EffectCancellation, QueueFull> {
self.inner
.try_open_file_dialog(spec, move |result| respond(result.map_err(Error)))
}
/// Queues a native service or returns backpressure; the token cancels pending delivery.
pub fn try_save_file_dialog<Message: Send + 'static>(
&mut self,
spec: SaveFileDialogSpec,
respond: impl Fn(Result<Option<PathBuf>, Error>) -> Message + Send + Sync + 'static,
) -> Result<EffectCancellation, QueueFull> {
self.inner
.try_save_file_dialog(spec, move |result| respond(result.map_err(Error)))
}
/// Queues a native service or returns backpressure; the token cancels pending delivery.
pub fn try_pick_directory<Message: Send + 'static>(
&mut self,
spec: DirectoryDialogSpec,
respond: impl Fn(Result<Option<PathBuf>, Error>) -> Message + Send + Sync + 'static,
) -> Result<EffectCancellation, QueueFull> {
self.inner
.try_pick_directory(spec, move |result| respond(result.map_err(Error)))
}
/// Queues a native service or returns backpressure; the token cancels pending delivery.
pub fn try_show_dialog<Message: Send + 'static>(
&mut self,
spec: NativeDialogSpec,
respond: impl Fn(Result<DialogResponse, Error>) -> Message + Send + Sync + 'static,
) -> Result<EffectCancellation, QueueFull> {
self.inner
.try_show_dialog(spec, move |result| respond(result.map_err(Error)))
}
/// Opens the native file-open dialog outside the update lock.
///
/// The dialog runs after the current update finishes, so it cannot
/// re-enter the event loop while application state is being reduced.
/// `respond` receives `Ok(None)` when the user cancels and produces the
/// message delivered back to `update` exactly once; the outcome is
/// dropped if the window is destroyed before the dialog finishes.
pub fn open_file_dialog<Message>(
&mut self,
spec: FileDialogSpec,
respond: impl Fn(Result<Option<Vec<PathBuf>>, Error>) -> Message + Send + Sync + 'static,
) where
Message: Send + 'static,
{
self.inner
.open_file_dialog(spec, move |result| respond(result.map_err(Error)));
}
/// Opens the native save dialog outside the update lock. `Ok(None)` means
/// the user cancelled.
pub fn save_file_dialog<Message>(
&mut self,
spec: SaveFileDialogSpec,
respond: impl Fn(Result<Option<PathBuf>, Error>) -> Message + Send + Sync + 'static,
) where
Message: Send + 'static,
{
self.inner
.save_file_dialog(spec, move |result| respond(result.map_err(Error)));
}
/// Opens a native directory picker outside the update lock. `Ok(None)`
/// means the user cancelled.
pub fn pick_directory<Message>(
&mut self,
spec: DirectoryDialogSpec,
respond: impl Fn(Result<Option<PathBuf>, Error>) -> Message + Send + Sync + 'static,
) where
Message: Send + 'static,
{
self.inner
.pick_directory(spec, move |result| respond(result.map_err(Error)));
}
/// Shows a native message or confirmation dialog outside the update lock
/// and delivers the typed [`DialogResponse`] back through `respond`.
pub fn show_dialog<Message>(
&mut self,
spec: NativeDialogSpec,
respond: impl Fn(Result<DialogResponse, Error>) -> Message + Send + Sync + 'static,
) where
Message: Send + 'static,
{
self.inner
.show_dialog(spec, move |result| respond(result.map_err(Error)));
}
}
/// A native window declaration that does not yet contain a view.
#[derive(Debug, Clone)]
pub struct WindowBuilder {
inner: crate::NativeWindowBuilder,
}
/// Creates a native window declaration.
pub fn window(title: impl Into<String>) -> WindowBuilder {
WindowBuilder {
inner: crate::NativeWindowBuilder::new(title),
}
}
impl WindowBuilder {
/// Sets the application identity used by desktop services.
pub fn app_name(mut self, name: impl Into<String>) -> Self {
self.inner = self.inner.app_name(name);
self
}
/// Sets the initial client size in logical pixels.
pub fn size(mut self, width: u32, height: u32) -> Self {
self.inner = self.inner.size(width, height);
self
}
/// Sets the minimum client size in logical pixels.
pub fn min_size(mut self, width: u32, height: u32) -> Self {
self.inner = self.inner.min_size(width, height);
self
}
/// Controls whether the user can resize the native window.
pub fn resizable(mut self, resizable: bool) -> Self {
self.inner = self.inner.resizable(resizable);
self
}
/// Controls whether the platform draws standard window decorations.
pub fn decorations(mut self, decorations: bool) -> Self {
self.inner = self.inner.decorations(decorations);
self
}
/// Lets the View draw the title bar while the platform keeps the frame.
#[cfg(feature = "window-chrome")]
pub fn custom_title_bar(mut self, custom_title_bar: bool) -> Self {
self.inner = self.inner.custom_title_bar(custom_title_bar);
self
}
/// Keeps the native window above ordinary windows when supported.
pub fn always_on_top(mut self, always_on_top: bool) -> Self {
self.inner = self.inner.always_on_top(always_on_top);
self
}
/// Uses a transparent host surface when supported by the platform.
pub fn transparent(mut self, transparent: bool) -> Self {
self.inner = self.inner.transparent(transparent);
self
}
/// Loads the platform window icon from an application-owned path.
pub fn icon_path(mut self, path: impl Into<String>) -> Self {
self.inner = self.inner.icon_path(path);
self
}
/// Releases the retained view and transient caches while hidden, keeping
/// application state and command routing alive.
pub fn release_view_when_hidden(mut self) -> Self {
self.inner = self.inner.release_view_when_hidden();
self
}
/// Attaches a non-visual rebuild handle to the window's stateful View.
///
/// Clone the handle into background work, update application-owned shared
/// state there, then call [`InvalidationHandle::request_rebuild`]. No
/// timer, animation or visible placeholder control is required. A handle
/// belongs to one window and this method must be followed by
/// [`WindowBuilder::stateful`]; fixed [`WindowBuilder::view`] content
/// cannot be rebuilt.
pub fn invalidation_handle(mut self, handle: InvalidationHandle) -> Self {
self.inner = self.inner.invalidation_handle(handle.inner);
self
}
/// Installs a fixed view and makes the window runnable.
pub fn view<Message: Clone>(mut self, view: Element<Message>) -> RunnableWindow {
self.inner = self.inner.view(view.into_raw());
RunnableWindow { inner: self.inner }
}
/// Installs the shared typed `State`/`Message`/`view`/`update` loop.
pub fn stateful<State, Message, ViewFn, UpdateFn>(
mut self,
state: State,
view: ViewFn,
update: UpdateFn,
) -> RunnableWindow
where
State: Send + 'static,
Message: Clone + Send + 'static,
ViewFn: Fn(&State) -> Element<Message> + Send + 'static,
UpdateFn: for<'a> Fn(&mut State, Message, &mut UpdateContext<'a>) + Send + 'static,
{
self.inner = self.inner.stateful_view(
state,
move |state| view(state).into_raw(),
move |state, message, context| {
update(state, message, &mut UpdateContext { inner: context });
},
);
RunnableWindow { inner: self.inner }
}
}
/// A native window with content that can enter the platform event loop.
#[derive(Debug, Clone)]
pub struct RunnableWindow {
inner: crate::NativeWindowBuilder,
}
impl RunnableWindow {
/// Creates the native host and runs its platform event loop until exit.
pub fn run(self) -> Result<(), Error> {
self.inner.run().map(|_| ()).map_err(Error)
}
}
/// Error returned by the stable application-authoring API.
#[derive(Debug)]
pub struct Error(crate::ZsuiError);
impl fmt::Display for Error {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(formatter)
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.0.source()
}
}
#[cfg(all(test, feature = "button", feature = "label"))]
mod tests {
use super::*;
#[derive(Clone)]
enum Message {
Increment,
}
struct State(u32);
#[test]
fn stable_stateful_window_uses_the_shared_live_runtime() {
let runnable = window("Stable API").size(360, 240).stateful(
State(0),
|state| {
column([
text(format!("Count: {}", state.0)),
button("Increment").on_click(Message::Increment),
])
},
|state, message, _context| match message {
Message::Increment => state.0 += 1,
},
);
assert!(runnable.inner.native_live_view_runtime().is_some());
}
}