rust_widgets 2.4.0

Pure Rust cross-platform native GUI library with hardware-adaptive rendering, 60+ 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
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
655
656
657
// SPDX-FileCopyrightText: Copyright (c) 2026 Mike Li/Mikewolfli/Wei Li(mikewolfli@163.com)
// SPDX-License-Identifier: MIT

//! One property contract, implemented by the control that owns the property.
//!
//! # Why this replaces the centralised dispatch
//!
//! Property access used to be a `match widget.kind()` tower split across 18
//! `include!`-ed files (`access_read_*.in.rs` / `access_write_*.in.rs`), organised
//! by *category* rather than by control. Adding a control meant editing seven
//! places — the `WidgetKind` variant, `properties*.in.rs`, the read and write
//! access files, `coercion.rs`, `constructors.rs` and `registration.rs` — and
//! missing any one of them produced a control that silently answered "no such
//! property". Fourteen controls had already fallen into that gap (eleven
//! `WebEngine*` types plus `Chip`, `CupertinoSwitch`, `Frame`, `GridTable`,
//! `MenuItem`), and a read cost nine sequential category probes before giving up.
//!
//! With this trait a control answers for itself: its properties live in its own
//! file, next to the fields they read. Adding one touches **one** file, and the
//! compiler enforces that the contract is met rather than trusting the author to
//! remember seven edits.
//!
//! # Common properties are inherited, not repeated
//!
//! `enabled` / `visible` / `tooltip` / `geometry` mean the same thing for every
//! control and live in [`BaseWidget`](crate::widget::base::BaseWidget). Writing
//! them 167 times is how a "unified" property layer drifts, so
//! [`base_property_get`] and [`base_property_set`] implement them once and a
//! control forwards its unmatched names there:
//!
//! ```ignore
//! impl WidgetProperties for Button {
//!     fn get(&self, name: &str) -> Result<CapabilityValue, CapabilityAccessError> {
//!         match name {
//!             "text" => Ok(CapabilityValue::String(self.text().to_string())),
//!             _ => base_property_get(self, name), // ← shared fallback
//!         }
//!     }
//!     // …
//! }
//! ```

use crate::compat::{format, String, ToString};
use crate::core::Rect;
use crate::widget::capability::types::{CapabilityAccessError, CapabilityValue};
use crate::widget::widget_trait::Widget;

/// The properties every control has through its [`crate::widget::BaseWidget`].
///
/// Exposed as a constant so a control's `property_names()` can concatenate rather
/// than retype them, and so tests and schema generation read one list.
pub const BASE_PROPERTY_NAMES: &[&str] = &["enabled", "visible", "tooltip", "geometry"];

/// Reads a property shared by every control.
///
/// A control calls this from the `_` arm of its own `get`, so the shared four are
/// implemented exactly once. Returns [`CapabilityAccessError::UnknownProperty`]
/// when the name is not shared, which lets the caller propagate it unchanged.
pub fn base_property_get(
    widget: &dyn Widget,
    name: &str,
) -> Result<CapabilityValue, CapabilityAccessError> {
    match name {
        "enabled" => Ok(CapabilityValue::Bool(widget.is_enabled())),
        "visible" => Ok(CapabilityValue::Bool(widget.is_visible())),
        "tooltip" => Ok(CapabilityValue::String(widget.tooltip().to_string())),
        "geometry" => Ok(geometry_to_value(widget.geometry())),
        _ => Err(CapabilityAccessError::UnknownProperty),
    }
}

/// Writes a property shared by every control.
///
/// The counterpart to [`base_property_get`]; a control forwards its `_` arm here.
pub fn base_property_set(
    widget: &mut dyn Widget,
    name: &str,
    value: CapabilityValue,
) -> Result<(), CapabilityAccessError> {
    match name {
        "enabled" => match value {
            CapabilityValue::Bool(enabled) => {
                widget.set_enabled(enabled);
                Ok(())
            }
            _ => Err(CapabilityAccessError::TypeMismatch),
        },
        "visible" => match value {
            CapabilityValue::Bool(visible) => {
                widget.set_visible(visible);
                Ok(())
            }
            _ => Err(CapabilityAccessError::TypeMismatch),
        },
        "tooltip" => match value {
            CapabilityValue::String(text) => {
                widget.set_tooltip(text);
                Ok(())
            }
            _ => Err(CapabilityAccessError::TypeMismatch),
        },
        // Geometry is deliberately read-only through this contract: a control's
        // rectangle is owned by the layout that placed it, and writing it here
        // would silently fight the layout on the next pass. Callers that mean to
        // move a control use `widget::runtime::set_geometry`.
        "geometry" => Err(CapabilityAccessError::ReadOnlyProperty),
        _ => Err(CapabilityAccessError::UnknownProperty),
    }
}

/// Encodes a rectangle as the tuple-shaped string the capability layer publishes.
///
/// Matches the format the previous centralised reader produced, so callers and
/// stored schema continue to parse it.
pub fn geometry_to_value(geometry: Rect) -> CapabilityValue {
    CapabilityValue::String(format!(
        "{},{},{},{}",
        geometry.x, geometry.y, geometry.width, geometry.height
    ))
}

/// The property contract for one control.
///
/// A control implements this in its own file and forwards unmatched names to
/// [`base_property_get`] / [`base_property_set`]. There is deliberately **no**
/// blanket `impl<T: Widget> WidgetProperties for T`: it would overlap every
/// concrete impl below (Rust coherence), which is why the shared four live in
/// plain functions instead of a default method.
///
/// # Where the name list comes from
///
/// `property_names()` names the properties directly, through the
/// [`crate::property_names_of`] macro, and appends [`BASE_PROPERTY_NAMES`] for the shared
/// four.
///
/// # Why not derive them from the schema table
///
/// The schema table (`properties_*.in.rs`) and this list are two statements of the
/// same fact, and an earlier draft of this doc-comment proposed deriving one from
/// the other with a `schema_names(BUTTON_PROPERTIES)` helper. `property_names` must
/// return a `'static` slice, and no `const fn` can project a slice of
/// `PropertySchema` structs into a slice of `&str`, so that helper cannot exist
/// without allocating on every call.
///
/// The two lists are therefore kept in step by test instead:
/// `schema_and_contract_publish_the_same_names` fails when they disagree. That is
/// the arrangement actually in force — this comment previously described a function
/// that was never written, which is worse than no comment at all.
///
/// # How a control declares the contract
///
/// ```ignore
/// impl WidgetProperties for Button {
///     fn get(&self, name: &str) -> Result<CapabilityValue, CapabilityAccessError> {
///         match name {
///             "text" => Ok(CapabilityValue::String(self.text().to_string())),
///             _ => base_property_get(self, name),
///         }
///     }
///     fn set(&mut self, name: &str, value: CapabilityValue) -> Result<(), CapabilityAccessError> {
///         match name {
///             "text" => { /* … */ Ok(()) }
///             _ => base_property_set(self, name, value),
///         }
///     }
///     fn property_names(&self) -> &'static [&'static str] {
///         property_names_of!["text", "pressed", "default", BASE_PROPERTY_NAMES]
///     }
/// }
/// ```
pub trait WidgetProperties {
    /// Reads a property by its stable lower-case name.
    fn get(&self, name: &str) -> Result<CapabilityValue, CapabilityAccessError>;

    /// Writes a property by its stable lower-case name.
    fn set(&mut self, name: &str, value: CapabilityValue) -> Result<(), CapabilityAccessError>;

    /// Names this control exposes, **including** the ones it inherits.
    ///
    /// The single source for schema generation, documentation and tests, so a
    /// property cannot be readable yet absent from the published list.
    ///
    /// # Why the shared four appear here too
    ///
    /// `enabled` / `visible` / `tooltip` / `geometry` are part of *this* control's
    /// published contract even though they are implemented once in
    /// [`base_property_get`]. A consumer that walks `property_names()` to build a
    /// property editor, a schema or a docs table must see them, and the pre-existing
    /// `*_PROPERTIES` tables listed them for exactly that reason. Omitting them would
    /// make a control look as if `enabled` did not exist.
    ///
    /// Controls therefore return `property_names_of!["own", .., BASE_PROPERTY_NAMES..]`
    /// — see [`crate::property_names_of`] for the const-compatible way to compose the two.
    fn property_names(&self) -> &'static [&'static str];

    /// The accepted spellings for an `Enum` property, or an empty slice.
    ///
    /// # Why this has a default
    ///
    /// Most controls publish no enum property, and many that do rely on the
    /// `impl_widget_property_hooks!` macro's own `*_PROPERTIES` table rather than writing
    /// this by hand. The default answers "no fixed set of values", which is correct for
    /// a non-enum property and is also the safe answer for an enum whose author has not
    /// listed tokens yet — it does not claim a set the control would then reject.
    ///
    /// # What an override must guarantee
    ///
    /// The tokens must be exactly the spellings `set` accepts. `widget_property_tokens`
    /// is the public reader; `capability::properties_tests` writes each returned token
    /// back through `set` and fails if any is refused, so a list that drifts from the
    /// control's parser is caught rather than shipped.
    fn property_tokens(&self, _name: &str) -> &'static [&'static str] {
        &[]
    }

    /// Runs an imperative command by its stable lower-case name.
    ///
    /// # What a command is, and why it is not a property write
    ///
    /// A property is state a caller **assigns** (`set_text`, `set_value`). A command
    /// is an action the control **performs** — `play`, `toggle`, `clear_selection` —
    /// whose effect may depend on state the caller does not own and whose result is an
    /// event, not a return value. `WidgetCapability::commands` has published a list of
    /// these names since the capability layer was introduced.
    ///
    /// # Why this method had to exist
    ///
    /// Until it did, `commands` was a promise with no way to keep it: the registry
    /// exported the names through
    /// [`WidgetCapability::commands`](crate::widget::capability::types::WidgetCapability::commands)
    /// and through the capability manifest, but **nothing dispatched them**. A
    /// generic consumer — a property editor building a command menu, a language
    /// binding, the declarative engine — could read a name, offer it, and get no
    /// effect and no error, because there was no call to make. Publishing an action a
    /// caller cannot invoke is the same defect shape as publishing a control that
    /// cannot be constructed (round 31) or a property that cannot be read (round 32).
    ///
    /// # The default
    ///
    /// `UnknownCommand` rather than `Ok(())`: a control with no override genuinely
    /// cannot run the command, and reporting success for an action that did not happen
    /// is the failure mode this whole contract exists to prevent. `Ok(())` means the
    /// command ran.
    ///
    /// # What an override must guarantee
    ///
    /// The set of names this answers must equal the `commands` list the control's
    /// capability publishes; `capability::properties_tests` calls every published name
    /// on a constructed control and fails if any is refused, so the list and the
    /// dispatch cannot drift.
    fn command(&mut self, name: &str) -> Result<(), CapabilityAccessError> {
        let _ = name;
        Err(CapabilityAccessError::UnknownCommand)
    }
}

/// Returns a `&'static [&'static str]` naming the given properties.
///
/// # Why a macro instead of a function
///
/// `WidgetProperties::property_names` must return a `'static` slice. Building one
/// from the existing `*_PROPERTIES` schema at run time would allocate (or leak) on
/// every call, and a `const fn` cannot project a slice of structs into a slice of
/// `&str`. Naming the properties once here keeps the declaration `const` and
/// allocation-free.
///
/// The shared four are appended via `BASE_PROPERTY_NAMES`, which carries its own
/// marks, so the macro accepts it as the last item:
///
/// ```ignore
/// fn property_names(&self) -> &'static [&'static str] {
///     property_names_of!["text", "pressed", "default", BASE_PROPERTY_NAMES]
/// }
/// ```
///
/// # Keeping it honest
///
/// The names must match what the control's `get` actually answers; the
/// `declared_names_are_all_readable` test in this module fails otherwise, so a
/// property cannot be published and unreadable at the same time.
#[macro_export]
macro_rules! property_names_of {
    ($($name:literal),* $(,)?) => {
        &[$($name),*]
    };
    ($($name:literal),* $(,)? BASE_PROPERTY_NAMES) => {
        &[$($name,)* "enabled", "visible", "tooltip", "geometry"]
    };
}

/// Emits the `dyn Widget` hooks that reach a control's [`WidgetProperties`] impl.
///
/// Invoke **inside** the control's `impl Widget for X` block, next to
/// [`crate::impl_draw_bridge!`]:
///
/// ```ignore
/// impl Widget for Button {
///     fn base(&self) -> &BaseWidget { &self.base }
///     fn base_mut(&mut self) -> &mut BaseWidget { &mut self.base }
///
///     impl_draw_bridge!();
///     impl_widget_property_hooks!();
/// }
/// ```
///
/// `Some(self)` is total here for the same reason as the drawing bridge: the type
/// is concrete and its `WidgetProperties` impl is checked by the compiler.
///
/// This is the property-layer analogue of [`crate::impl_draw_bridge!`], and it
/// exists for the same reason: `&mut dyn Widget` cannot select a concrete impl
/// without a downcast, so the override has to be generated where the concrete type
/// is still visible.
#[macro_export]
macro_rules! impl_widget_property_hooks {
    () => {
        fn properties_dyn(
            &self,
        ) -> Option<&dyn $crate::widget::capability::properties_trait::WidgetProperties> {
            Some(self)
        }

        fn properties_dyn_mut(
            &mut self,
        ) -> Option<&mut dyn $crate::widget::capability::properties_trait::WidgetProperties> {
            Some(self)
        }
    };
}

/// Convenience forwarding for a `&dyn Widget`, used by the reflection entry
/// points that still hold a trait object.
///
/// A widget that wants this behaviour implements [`WidgetProperties`] on its
/// concrete type; this helper downcasts and calls it, returning
/// [`CapabilityAccessError::UnsupportedOnWidget`] when the concrete type has no
/// impl yet. That is the honest answer, and it keeps the migration incremental:
/// a control without an impl behaves exactly as before rather than panicking.
pub fn widget_property_get(
    widget: &dyn Widget,
    name: &str,
) -> Result<CapabilityValue, CapabilityAccessError> {
    widget
        .properties_dyn()
        .map_or(Err(CapabilityAccessError::UnsupportedOnWidget), |props| props.get(name))
}

/// Write-side counterpart to [`widget_property_get`].
pub fn widget_property_set(
    widget: &mut dyn Widget,
    name: &str,
    value: CapabilityValue,
) -> Result<(), CapabilityAccessError> {
    match widget.properties_dyn_mut() {
        Some(props) => props.set(name, value),
        None => Err(CapabilityAccessError::UnsupportedOnWidget),
    }
}

/// Returns a control's declared property names, or an empty slice when it has no
/// [`WidgetProperties`] impl.
///
/// `None` and an empty slice mean different things and both are useful: `None` is
/// "this control has not migrated yet", `&[]` is "this control declares, on
/// purpose, that it has no properties" — the honest answer for the `WebEngine*`
/// types, which expose no widget properties at all.
pub fn widget_property_names(widget: &dyn Widget) -> Option<&'static [&'static str]> {
    widget.properties_dyn().map(WidgetProperties::property_names)
}

/// Returns the accepted spellings for an `Enum` property, or an empty slice.
///
/// # Why a caller needs this
///
/// An enum property is written as one of a fixed set of tokens (`"single"`,
/// `"multiple"`, `"ascending"` …), but those tokens were only discoverable by reading
/// the control's source. A caller building a property editor or validating user input
/// had to hard-code its own copy of the list, and nothing failed when the control's
/// parser changed — the copy just silently went stale.
///
/// The answer comes from the control's own [`PropertySchema`], so it cannot drift from
/// what `set` accepts without the control's declaration changing too.
///
/// # Empty is a real answer
///
/// An empty slice means "this property declares no fixed set of values". That covers
/// both a non-enum property and an enum whose author has not listed its tokens yet. It
/// is not an error: most properties are not enums.
///
/// # Which schema
///
/// Looked up through the same registry path as the other reflection entry points, so a
/// control in a profile without the capability registry answers empty rather than
/// failing.
///
/// [`PropertySchema`]: crate::widget::capability::types::PropertySchema
pub fn widget_property_tokens(widget: &dyn Widget, name: &str) -> &'static [&'static str] {
    widget.properties_dyn().map_or(&[], |props| props.property_tokens(name))
}

/// Appends one item to a control that holds a list of strings.
///
/// # Why this is a downcast and not a property write
///
/// The `item_count` property is deliberately **read-only** on every control that
/// publishes it (`list_box`, `combo_box`, `list_view`, …): a count is a
/// consequence of the items, not a settable value, and letting a caller write it
/// would desynchronise it from the actual collection. So the only honest way to
/// grow a collection is the control's own method, which is what this dispatches
/// to.
///
/// Returns `false` when the control is not one that holds items. That is a real
/// "no", not a silent success: a caller adding to a `Button` should be told.
pub fn append_widget_list_item(widget: &mut dyn Widget, item: String) -> bool {
    use crate::widget::capability::coercion::widget_as_mut;
    if let Some(list) = widget_as_mut::<crate::widget::ListBox>(widget) {
        list.add_item(item);
        return true;
    }
    if let Some(combo) = widget_as_mut::<crate::widget::ComboBox>(widget) {
        combo.add_item(item);
        return true;
    }
    false
}

/// Removes every item from a control that holds a list of strings.
///
/// Returns `false` when the control does not hold items. See
/// [`append_widget_list_item`] for why this is not a property write.
pub fn clear_widget_list_items(widget: &mut dyn Widget) -> bool {
    use crate::widget::capability::coercion::widget_as_mut;
    if let Some(list) = widget_as_mut::<crate::widget::ListBox>(widget) {
        list.clear();
        return true;
    }
    if let Some(combo) = widget_as_mut::<crate::widget::ComboBox>(widget) {
        combo.clear();
        return true;
    }
    false
}

/// Returns how many items a control holds, or `0` when it holds none or does not
/// hold items.
pub fn widget_list_item_count(widget: &dyn Widget) -> usize {
    use crate::widget::capability::coercion::widget_as;
    if let Some(list) = widget_as::<crate::widget::ListBox>(widget) {
        return list.count();
    }
    if let Some(combo) = widget_as::<crate::widget::ComboBox>(widget) {
        return combo.count();
    }
    0
}

/// Reads one item's text out of a control that holds a list of strings.
///
/// # The gap this closes
///
/// `item_count` was the only collection fact readable through the property surface.
/// A caller could `add` items, count them and clear them, but could never read back
/// what it had added — so a control's contents were write-only across the whole
/// declarative API, and a test asserting "the items are what I set" was impossible to
/// write without downcasting to the concrete type.
///
/// Returns `None` in three cases, all of which are honestly "no value": the control
/// does not hold items, `index` is past the last item, or the item at `index` holds no
/// text. A caller that needs to tell them apart asks [`widget_list_item_count`] first.
pub fn widget_list_item(widget: &dyn Widget, index: usize) -> Option<String> {
    use crate::widget::capability::coercion::widget_as;
    if let Some(list) = widget_as::<crate::widget::ListBox>(widget) {
        return list.item(index).map(str::to_string);
    }
    if let Some(combo) = widget_as::<crate::widget::ComboBox>(widget) {
        return combo.item(index).map(str::to_string);
    }
    None
}

/// The contract path, with no fallback.
///
/// # Why there is only one path now
///
/// This used to try the control's own `WidgetProperties` impl and, on
/// `UnsupportedOnWidget`, delegate to a centralised nine-category probe over the
/// property tables (BLUE15 Phase C-1). That second path duplicated the contract
/// for every control that had migrated — two places answering "what properties does
/// this control have", which is exactly the drift the contract exists to prevent.
///
/// Every registered control now implements `WidgetProperties`, and a test asserts
/// it, so nothing can reach the fallback. It has been deleted rather than left as
/// dead code, and this function is consequently a plain forward.
pub fn read_widget_property_by_name(
    widget: &dyn Widget,
    name: &str,
) -> Result<CapabilityValue, CapabilityAccessError> {
    widget_property_get(widget, name)
}

/// Write-side counterpart to [`read_widget_property_by_name`].
pub fn write_widget_property_by_name(
    widget: &mut dyn Widget,
    name: &str,
    value: CapabilityValue,
) -> Result<(), CapabilityAccessError> {
    widget_property_set(widget, name, value)
}
#[cfg(test)]
mod tests {
    use super::*;
    use crate::core::Rect;
    use crate::widget::base::BaseWidget;
    use crate::widget::WidgetKind;

    /// A minimal widget used to pin the shared-property contract in isolation.
    struct Probe {
        base: BaseWidget,
    }

    impl crate::widget::widget_trait::Widget for Probe {
        fn base(&self) -> &BaseWidget {
            &self.base
        }
        fn base_mut(&mut self) -> &mut BaseWidget {
            &mut self.base
        }

        // Without these the type would implement `WidgetProperties` yet be
        // unreachable through `dyn Widget`, which is exactly the failure mode the
        // dispatcher tests below exist to catch.
        crate::impl_widget_property_hooks!();
    }

    impl crate::event::EventHandler for Probe {
        fn handle_event(&mut self, _event: &crate::event::Event) {}
    }

    impl WidgetProperties for Probe {
        fn get(&self, name: &str) -> Result<CapabilityValue, CapabilityAccessError> {
            base_property_get(self, name)
        }
        fn set(&mut self, name: &str, value: CapabilityValue) -> Result<(), CapabilityAccessError> {
            base_property_set(self, name, value)
        }
        fn property_names(&self) -> &'static [&'static str] {
            BASE_PROPERTY_NAMES
        }
    }

    fn probe() -> Probe {
        Probe { base: BaseWidget::new(WidgetKind::Panel, Rect::new(1, 2, 30, 40), "probe") }
    }

    /// Every shared property must round-trip, so forwarding to the base helpers
    /// is not merely present but correct.
    #[test]
    fn base_properties_round_trip() {
        let mut widget = probe();

        assert_eq!(widget.get("enabled"), Ok(CapabilityValue::Bool(true)));
        widget.set("enabled", CapabilityValue::Bool(false)).expect("writable");
        assert_eq!(widget.get("enabled"), Ok(CapabilityValue::Bool(false)));

        widget.set("visible", CapabilityValue::Bool(false)).expect("writable");
        assert_eq!(widget.get("visible"), Ok(CapabilityValue::Bool(false)));

        widget.set("tooltip", CapabilityValue::String("tip".into())).expect("writable");
        assert_eq!(widget.get("tooltip"), Ok(CapabilityValue::String("tip".into())));
    }

    /// A wrong-typed write must be rejected rather than silently coerced.
    #[test]
    fn base_property_set_rejects_type_mismatch() {
        let mut widget = probe();
        assert_eq!(
            widget.set("enabled", CapabilityValue::String("yes".into())),
            Err(CapabilityAccessError::TypeMismatch)
        );
    }

    /// Geometry must be readable and honestly read-only, not writable.
    #[test]
    fn geometry_is_readable_but_read_only() {
        let mut widget = probe();
        assert_eq!(widget.get("geometry"), Ok(CapabilityValue::String("1,2,30,40".into())));
        assert_eq!(
            widget.set("geometry", CapabilityValue::String("0,0,1,1".into())),
            Err(CapabilityAccessError::ReadOnlyProperty)
        );
    }

    /// An unknown name must be reported as such, not as "not supported here",
    /// so a caller can tell a typo from a control that has not migrated.
    #[test]
    fn unknown_names_are_distinguished() {
        let widget = probe();
        assert_eq!(widget.get("nope"), Err(CapabilityAccessError::UnknownProperty));
    }

    /// A property the contract rejects definitively must not be handed to the
    /// legacy tables.
    ///
    /// `ReadOnlyProperty` is a decision, not a gap: if the fallback saw it, a
    /// control could declare a property read-only and still have an older category
    /// arm write it (BLUE15 Phase C-1).
    #[test]
    fn definitive_contract_answers_are_not_delegated() {
        let mut widget = probe();
        assert_eq!(
            write_widget_property_by_name(
                &mut widget,
                "geometry",
                CapabilityValue::String("0,0,1,1".into()),
            ),
            Err(CapabilityAccessError::ReadOnlyProperty),
        );
    }

    /// The dispatcher must reach the contract through `dyn Widget`, not only on
    /// the concrete type.
    ///
    /// This pins the whole point of the reflection hooks: a control can implement
    /// `WidgetProperties` and still be invisible if its `impl Widget` forgets them.
    /// An unknown name then answers from the contract (`UnknownProperty`) instead
    /// of falling through to "this control has no contract at all".
    #[test]
    fn the_dispatcher_reaches_the_contract_through_dyn_widget() {
        let mut widget = probe();
        let dynamic: &mut dyn crate::widget::Widget = &mut widget;
        assert_eq!(
            write_widget_property_by_name(dynamic, "not_a_property", CapabilityValue::Bool(true)),
            Err(CapabilityAccessError::UnknownProperty),
        );
        let dynamic: &dyn crate::widget::Widget = &widget;
        assert_eq!(
            read_widget_property_by_name(dynamic, "not_a_property"),
            Err(CapabilityAccessError::UnknownProperty),
        );
        assert_eq!(
            read_widget_property_by_name(dynamic, "enabled"),
            Ok(CapabilityValue::Bool(true))
        );
    }

    /// The published name list must match what `get` actually answers, so schema
    /// generation and reflection cannot disagree.
    #[test]
    fn declared_names_are_all_readable() {
        let widget = probe();
        for name in widget.property_names() {
            assert!(
                widget.get(name).is_ok(),
                "property_names() declares {name:?} but get() rejects it"
            );
        }
    }
}