rust_widgets 2.5.2

Pure Rust cross-platform native GUI library with hardware-adaptive rendering, 180 widgets, touch/gesture support, i18n, and SVG-pipeline-accurate output
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
#![cfg(all(
    any(feature = "desktop", feature = "tablet", feature = "mobile"),
    not(any(feature = "mini", feature = "embedded"))
))]

//! R6 Platform Capability Matrix Integration Test (blue9_r6)
//!
//! This test verifies:
//! 1. The capability matrix document exists and is parseable
//! 2. Basic platform contract negotiation works
//! 3. All WidgetKind variants have at least StateBacked capability

use std::path::Path;

// ---------------------------------------------------------------------------
// Test 1: Capability matrix document exists and is parseable
// ---------------------------------------------------------------------------

#[test]
fn capability_matrix_document_exists() {
    let matrix_path = Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("docs")
        .join("plans")
        .join("platform_capability_matrix.md");

    assert!(
        matrix_path.exists(),
        "Capability matrix document not found at: {}",
        matrix_path.display()
    );

    let content =
        std::fs::read_to_string(&matrix_path).expect("Failed to read capability matrix document");

    assert!(content.contains("# Platform Capability Matrix"), "Matrix missing title header");
    assert!(content.contains("| Widget |"), "Matrix missing table header");
    assert!(
        content.contains("") || content.contains("🔶"),
        "Matrix missing capability emoji codes"
    );
}

#[test]
fn capability_matrix_covers_all_platforms() {
    let matrix_path = Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("docs")
        .join("plans")
        .join("platform_capability_matrix.md");

    let content =
        std::fs::read_to_string(&matrix_path).expect("Failed to read capability matrix document");

    let required_platforms =
        ["Windows", "Linux/X11", "macOS", "Wayland", "Mobile", "Harmony", "Embedded/Stub"];

    for platform in &required_platforms {
        assert!(content.contains(platform), "Matrix missing platform column: {}", platform);
    }
}

// ---------------------------------------------------------------------------
// Test 2: Platform contract negotiation works
// ---------------------------------------------------------------------------

#[test]
fn platform_contract_negotiation_works() {
    use rust_widgets::core::RuntimeProfile;
    use rust_widgets::platform::negotiate_capability_contract;
    use rust_widgets::platform::CapabilityContract;

    let full_contract = negotiate_capability_contract(RuntimeProfile::Full);
    match full_contract {
        CapabilityContract::Native(contract) => {
            assert!(
                contract.typed_widget_trigger,
                "Native contract must support typed_widget_trigger"
            );
            // The fallback used to fabricate all-true, so a backend that publishes no
            // contract of its own was told it had a native menu even while reporting a
            // non-desktop family. The answer must now follow the family the backend
            // actually reports.
            let actual_family = rust_widgets::platform::get_platform().family();
            let expected = rust_widgets::platform::default_capabilities_for(actual_family);
            assert_eq!(
                contract, expected,
                "the negotiated contract must match the family the backend reports ({actual_family:?}); \
                 a mismatch means the fallback invented capabilities"
            );
        }
        CapabilityContract::Embedded(contract) => {
            assert!(
                contract.typed_widget_trigger,
                "Embedded contract must support typed_widget_trigger"
            );
        }
    }

    let embedded_contract = negotiate_capability_contract(RuntimeProfile::Embedded);
    match embedded_contract {
        CapabilityContract::Native(_) => {
            // On test environments without embedded, we get native fallback
        }
        CapabilityContract::Embedded(contract) => {
            assert!(
                contract.typed_widget_trigger,
                "Embedded contract must support typed_widget_trigger"
            );
            assert!(
                contract.low_memory_mode,
                "Embedded contract should have low_memory_mode enabled"
            );
        }
    }
}

#[test]
fn platform_capabilities_have_typed_widget_trigger() {
    use rust_widgets::platform::PlatformCapabilities;

    let desktop_caps = PlatformCapabilities {
        dpi_scaling: true,
        ime: true,
        accessibility: true,
        native_menu: true,
        typed_widget_trigger: true,
    };
    assert!(desktop_caps.typed_widget_trigger);
    assert!(desktop_caps.dpi_scaling);
    assert!(desktop_caps.ime);

    let embedded_caps = PlatformCapabilities {
        dpi_scaling: false,
        ime: false,
        accessibility: false,
        native_menu: false,
        typed_widget_trigger: true,
    };
    assert!(embedded_caps.typed_widget_trigger);
}

// ---------------------------------------------------------------------------
// Test 3: All WidgetKind variants have at least StateBacked capability
// ---------------------------------------------------------------------------

#[test]
fn all_widget_kinds_have_non_empty_debug_repr() {
    use rust_widgets::widget::kind::WidgetKind;

    // Every WidgetKind variant must have a non-empty Debug representation
    let kinds = [
        WidgetKind::Window,
        WidgetKind::Dialog,
        WidgetKind::MessageBox,
        WidgetKind::FileDialog,
        WidgetKind::ColorDialog,
        WidgetKind::FontDialog,
        WidgetKind::InputDialog,
        WidgetKind::ProgressDialog,
        WidgetKind::PopupWindow,
        WidgetKind::Button,
        WidgetKind::CheckBox,
        WidgetKind::RadioButton,
        WidgetKind::Label,
        WidgetKind::LineEdit,
        WidgetKind::TextEdit,
        WidgetKind::RichEdit,
        WidgetKind::ComboBox,
        WidgetKind::SpinBox,
        WidgetKind::ListBox,
        WidgetKind::ListView,
        WidgetKind::TreeView,
        WidgetKind::ProgressBar,
        WidgetKind::Slider,
        WidgetKind::ScrollBar,
        WidgetKind::ScrollArea,
        WidgetKind::Panel,
        WidgetKind::DockPanel,
        WidgetKind::GroupBox,
        WidgetKind::TabWidget,
        WidgetKind::Splitter,
        WidgetKind::MdiArea,
        WidgetKind::MenuBar,
        WidgetKind::Menu,
        WidgetKind::MenuItem,
        WidgetKind::ContextMenu,
        WidgetKind::ToolBar,
        WidgetKind::StatusBar,
        WidgetKind::Canvas,
        WidgetKind::Table,
        WidgetKind::Grid,
        WidgetKind::Chart,
        WidgetKind::ToggleButton,
        WidgetKind::CheckListBox,
        WidgetKind::DoubleSpinBox,
        WidgetKind::Dial,
        WidgetKind::Wizard,
        WidgetKind::DatePicker,
        WidgetKind::TimePicker,
        WidgetKind::DateTimePicker,
        WidgetKind::DirectoryDialog,
        WidgetKind::DataView,
        WidgetKind::PropertyGrid,
        WidgetKind::Toolbox,
        WidgetKind::StackedWidget,
        WidgetKind::CollapsiblePane,
        WidgetKind::DockWidget,
        WidgetKind::ActivityIndicator,
        WidgetKind::Calendar,
        WidgetKind::ColumnView,
        WidgetKind::UndoView,
        WidgetKind::CommandLink,
        WidgetKind::LCDNumber,
        WidgetKind::FontComboBox,
        WidgetKind::WebEngineView,
        WidgetKind::Action,
        WidgetKind::ToolButton,
        WidgetKind::FreeformShape,
        WidgetKind::TabBar,
        WidgetKind::PieMenu,
        WidgetKind::RibbonBar,
    ];

    for kind in &kinds {
        let debug_str = format!("{:?}", kind);
        assert!(!debug_str.is_empty(), "WidgetKind variant has an empty debug representation");
    }
}

#[test]
fn widget_kind_variants_are_exhaustive() {
    // The capability matrix must cover the same widget set as the `WidgetKind`
    // enum source. Both sides are derived dynamically rather than from a count,
    // because a count drifts silently: the enum grew 82 -> 167 -> 169 over time,
    // and a hard-coded number would have been "updated" rather than investigated.
    //
    // # The one documented difference
    //
    // The matrix lists the `WebEngine*` **wrapper types** as well as
    // `WebEngineView`. They are real render-pipeline symbols over the one
    // registered view — each forwards `Widget::base()` to what it wraps, so
    // `kind()` answers `WebEngineView` for all of them. They were once
    // `WidgetKind` variants marked `kind-role: base`, which made them orphans
    // (principle #22): nothing could produce them, and `create_web_engine_page(..)`
    // produced id `0` because `factory_name_for_kind` resolves through
    // `capability_by_kind`.
    //
    // So the comparison is: every `WidgetKind` variant has a matrix row, and every
    // matrix row is either a variant or one of the documented wrapper names. The
    // assertion stays exact in both directions — it is the *set* that is allowed
    // to be larger, not the count that is loosened.
    const WEB_ENGINE_WRAPPERS: &[&str] = &[
        "WebEnginePage",
        "WebEngineSettings",
        "WebEngineDownloadItem",
        "WebEngineCookieStore",
        "WebEngineWebChannel",
        "WebEngineFindTextResult",
        "WebEngineNotification",
        "WebEngineScriptDialog",
        "WebEngineContextMenuRequest",
    ];

    let kind_src =
        std::fs::read_to_string(Path::new(env!("CARGO_MANIFEST_DIR")).join("src/widget/kind.rs"))
            .expect("failed to read src/widget/kind.rs");
    let kind_variants: std::collections::BTreeSet<String> = kind_src
        .lines()
        .filter(|line| {
            let trimmed = line.trim();
            trimmed.ends_with(',') && trimmed.chars().next().is_some_and(|c| c.is_ascii_uppercase())
        })
        .map(|line| line.trim().trim_end_matches(',').to_string())
        .collect();

    let matrix_path = Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("docs")
        .join("plans")
        .join("platform_capability_matrix.md");
    let content = std::fs::read_to_string(&matrix_path).expect("failed to read capability matrix");
    let matrix_rows: std::collections::BTreeSet<String> = content
        .lines()
        .filter(|line| line.starts_with("| **"))
        .filter_map(|line| {
            let rest = line.strip_prefix("| **")?;
            let end = rest.find("**")?;
            Some(rest[..end].to_string())
        })
        .collect();

    assert!(
        kind_variants.len() >= 100,
        "kind.rs unit-variant extraction looks wrong: got {}",
        kind_variants.len()
    );
    assert!(!matrix_rows.is_empty(), "the capability matrix has no widget rows");

    // Every kind must be documented.
    let undocumented: Vec<&String> = kind_variants.difference(&matrix_rows).collect();
    assert!(
        undocumented.is_empty(),
        "these WidgetKind variants have no capability-matrix row: {undocumented:?}"
    );

    // And every row must be a kind or a documented wrapper — no invented rows.
    let unexplained: Vec<&String> = matrix_rows
        .difference(&kind_variants)
        .filter(|name| !WEB_ENGINE_WRAPPERS.contains(&name.as_str()))
        .collect();
    assert!(
        unexplained.is_empty(),
        "these capability-matrix rows are neither a WidgetKind variant nor a \
         documented WebEngine wrapper, so the matrix documents something that does \
         not exist: {unexplained:?}"
    );

    // The wrapper list must not silently rot: each name has to be a row.
    for wrapper in WEB_ENGINE_WRAPPERS {
        assert!(
            matrix_rows.contains(*wrapper),
            "{wrapper} is excused as a WebEngine wrapper but has no matrix row; if it \
             was deleted, drop it from WEB_ENGINE_WRAPPERS too"
        );
    }
}

// ---------------------------------------------------------------------------
// Test 4: Verify matrix document is consistent with WidgetKind source
// ---------------------------------------------------------------------------

#[test]
fn capability_matrix_matches_widget_kind() {
    let matrix_path = Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("docs")
        .join("plans")
        .join("platform_capability_matrix.md");

    let content =
        std::fs::read_to_string(&matrix_path).expect("Failed to read capability matrix document");

    // Extract widget names from the matrix table rows.
    // Rows look like: "| **Window** | ✅ | ✅ | ..."
    let matrix_widgets: Vec<String> = content
        .lines()
        .filter_map(|line| {
            if line.starts_with("| **") {
                let rest = line.strip_prefix("| **")?;
                let name = rest.split("**").next()?;
                Some(name.to_string())
            } else {
                None
            }
        })
        .collect();

    // Check that a representative sample of key widgets is present
    let required_widgets = [
        "Window",
        "Button",
        "Label",
        "ListView",
        "MenuBar",
        "Menu",
        "ToolBar",
        "StatusBar",
        "Dialog",
        "Canvas",
        "Toolbox",
        "RibbonBar",
    ];

    for widget in &required_widgets {
        assert!(
            matrix_widgets.contains(&widget.to_string()),
            "Widget '{}' is not present in the capability matrix",
            widget
        );
    }

    // Verify the matrix widget count is reasonable (matches WidgetKind variants)
    assert!(
        matrix_widgets.len() >= 80,
        "Matrix only has {} widget rows (expected >= 80)",
        matrix_widgets.len()
    );
}

// ---------------------------------------------------------------------------
// Test 8: The negotiation fallback must not invent capabilities
// ---------------------------------------------------------------------------
//
// `negotiate_capability_contract(Full)` falls back when a backend publishes no
// `native_capability_contract()`. `Platform::native_capability_contract()` returns
// `None` for every non-`Desktop` family, so the fallback is only ever reached from a
// **non-desktop** backend — and it used to answer with all four flags `true`, telling
// an embedded or mobile backend it had DPI scaling, IME, accessibility and a native
// menu.
//
// This drives the **real** entry point with a real non-desktop backend installed
// through `with_platform`, rather than asserting on the helper the fallback happens to
// call. That distinction matters: a test written against `default_capabilities_for`
// would keep passing if the fallback stopped using it, which is exactly the bug.
#[test]
fn capability_fallback_follows_the_family_and_never_invents() {
    use rust_widgets::core::{PlatformFamily, RuntimeProfile};
    use rust_widgets::platform::{
        default_capabilities_for, negotiate_capability_contract, with_platform, CapabilityContract,
        StubPlatform,
    };

    // A backend that publishes no `native_capability_contract()` of its own, because
    // its family is not `Desktop`. This is the only input that reaches the fallback.
    // `with_platform` needs a `&'static`, so the two are leaked once per test run.
    static MOBILE_BACKEND: std::sync::LazyLock<StubPlatform> =
        std::sync::LazyLock::new(|| StubPlatform::new("test-mobile", PlatformFamily::Mobile));
    static EMBEDDED_BACKEND: std::sync::LazyLock<StubPlatform> =
        std::sync::LazyLock::new(|| StubPlatform::new("test-embedded", PlatformFamily::Embedded));

    for (name, backend, family) in [
        ("mobile", &*MOBILE_BACKEND as &'static dyn rust_widgets::platform::Platform,
         PlatformFamily::Mobile),
        ("embedded", &*EMBEDDED_BACKEND as &'static dyn rust_widgets::platform::Platform,
         PlatformFamily::Embedded),
    ] {
        // The precondition this test depends on: no published contract, so the
        // fallback really is what answers.
        assert!(
            backend.native_capability_contract().is_none(),
            "{name}: this test only means something for a backend without a published contract"
        );

        let contract = with_platform(backend, || {
            negotiate_capability_contract(RuntimeProfile::Full)
        });
        let CapabilityContract::Native(caps) = contract else {
            panic!("{name}: a Full profile must negotiate a Native contract");
        };
        let expected = default_capabilities_for(family);

        assert_eq!(
            caps, expected,
            "{name}: the fallback contract must match the family the backend reports; \n\
             a mismatch means it invented capabilities the host cannot serve"
        );
        assert!(
            !(caps.dpi_scaling && caps.ime && caps.accessibility && caps.native_menu),
            "{name}: a non-desktop backend must never receive an all-true contract"
        );
        assert!(
            caps.typed_widget_trigger,
            "{name}: typed triggers come from the library, never the host"
        );
    }
}