rust_widgets 2.8.7

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
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
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
// SPDX-FileCopyrightText: Copyright (c) 2026 Mike Li/Mikewolfli/Wei Li(mikewolfli@163.com)
// SPDX-License-Identifier: MIT

//! Typed records and record lists — the carrier for a control's *collection* property (BLUE26
//! B26.09).
//!
//! # The gap this closes
//!
//! Several controls hold a list of records rather than a scalar: `chip` holds `ChipItem { id,
//! label, selected }`, `timeline_widget` holds `TimelineItem { id, label, start, end }`,
//! `segmented_control` holds `SegmentItem { id, label }`. Their Rust APIs are typed, but the
//! *property contract* could not express any of them:
//!
//! * `PropertyValueKind` had no list-of-records kind, and
//! * [`CapabilityValue`](super::CapabilityValue) had no record carrier, so a JSON document's
//!   `"items": [{ "id": "a", "label": "A" }]` was **dropped on the floor** — `ProjectNode::
//!   scalar_properties` skips arrays and objects, which made the value silently absent rather than
//!   an error.
//!
//! The tempting shortcut was to carry the list as its JSON spelling in a `String` property. That is
//! refused here for the same reason [`Constraint`](super::Constraint) is enforced rather than
//! documented: a stringified list is *accepted* by any setter, so a typo in a field name produces a
//! control nothing like the document asked for and no error anywhere. A record list that arrives
//! typed can be checked against the schema the control publishes, and refused with a reason.
//!
//! # What is here, and what is deliberately not
//!
//! There is **one** new value shape: an ordered list of records that share one declared schema.
//!
//! * A **row move** is [`RecordList::move_item`]; a **column move** is [`RecordList::move_field`].
//!   Both preserve every item's [`id`](RecordSchema::id_field), which is what lets a host keep a
//!   selection, a scroll anchor or an undo record across a reorder.
//! * There is no single-record value and no nested record. A control that needs those has not been
//!   named yet, and `CapabilityValue::Tuple` already carries the positional shapes that do exist
//!   (`heatmap.set_cell` is a 3-tuple, `range_slider.range_changed` a 2-tuple).
//! * The library holds **no business state**. A `RecordList` is a value the caller owns and hands
//!   over; the control copies what it needs into its own storage. Applying a value is atomic — see
//!   [`RecordList::validate`] — so a refused update cannot leave a half-replaced list behind.

use super::types::{CapabilityValue, PropertyValueKind};
use crate::compat::{format, String, Vec};

/// One field of a record schema: its name, its declared kind, and whether a record must carry it.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RecordField {
    /// The field's name, as it appears in a document and in [`RecordValue::get`].
    pub name: &'static str,
    /// The declared kind of this field's value.
    pub kind: PropertyValueKind,
    /// Whether every record must carry this field.
    ///
    /// A missing **required** field is [`RecordViolation::MissingField`]. A missing optional field
    /// is not a violation, and reading it answers `None` — the distinction a property editor needs
    /// to render "unset" differently from "wrong".
    pub required: bool,
}

impl RecordField {
    /// A required field of the given kind. Shorthand for the schema tables.
    pub const fn required(name: &'static str, kind: PropertyValueKind) -> Self {
        Self { name, kind, required: true }
    }

    /// An optional field of the given kind.
    pub const fn optional(name: &'static str, kind: PropertyValueKind) -> Self {
        Self { name, kind, required: false }
    }
}

/// The declared shape of one record in a list-valued property.
///
/// # Why the identity field is declared rather than assumed
///
/// A record list is *ordered*, so an item's position is not its identity: a row move, a sorted
/// insert or a filtered view all change positions while the item stays the same item. The list
/// therefore has to name the field that carries identity, and a schema whose `id_field` is not one
/// of its own fields is a schema defect ([`RecordViolation::SchemaHasNoIdField`]) rather than a
/// list that quietly cannot answer [`RecordList::get_by_id`].
///
/// # Why uniqueness is a schema decision
///
/// Most collections key on their id, and a duplicate is then a defect, not a choice. Some
/// collections deliberately allow a repeated id — a timeline showing the same event twice — so the
/// schema states which it is instead of the validator guessing.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RecordSchema {
    /// The record's fields, in the order a designer should offer them.
    pub fields: &'static [RecordField],
    /// The field that carries an item's stable identity.
    pub id_field: &'static str,
    /// Whether two records in one list may carry the same id.
    pub ids_unique: bool,
    /// Whether a record's id must be non-empty.
    pub ids_required: bool,
}

impl RecordSchema {
    /// The field with this name, if the schema declares one.
    pub fn field(&self, name: &str) -> Option<&'static RecordField> {
        self.fields.iter().find(|field| field.name == name)
    }

    /// The position of the field with this name, in declaration order.
    pub fn field_index(&self, name: &str) -> Option<usize> {
        self.fields.iter().position(|field| field.name == name)
    }

    /// The declared field names, in declaration order.
    pub fn names(&self) -> Vec<&'static str> {
        self.fields.iter().map(|field| field.name).collect()
    }

    /// The field the schema names as its identity, or `None` when it names none.
    ///
    /// `None` is what [`RecordList::validate`] reports as
    /// [`RecordViolation::SchemaHasNoIdField`]: a list whose schema cannot answer "which field is
    /// the identity" cannot be checked for a duplicate either.
    pub fn id_field_schema(&self) -> Option<&'static RecordField> {
        self.field(self.id_field)
    }
}

/// One record: an ordered group of named values.
///
/// The order is the order the caller supplied, which is what a `Vec`-backed field list preserves;
/// [`RecordList::move_field`] is the one operation that changes it, and it changes it for every
/// record at once so the list stays rectangular.
///
/// Not a map: two fields with the same name would be a silent last-one-wins, and the schema check
/// that follows would then be judging a record whose real shape no longer matches the document.
#[derive(Debug, Clone, PartialEq, Default)]
pub struct RecordValue {
    fields: Vec<(String, CapabilityValue)>,
}

impl RecordValue {
    /// An empty record.
    pub fn new() -> Self {
        Self { fields: Vec::new() }
    }

    /// Adds one field, chainable.
    ///
    /// A repeated name is kept as a second entry rather than replacing the first: the duplicate is
    /// then visible to [`RecordList::validate`] instead of being resolved silently here.
    pub fn with(mut self, name: impl Into<String>, value: CapabilityValue) -> Self {
        self.fields.push((name.into(), value));
        self
    }

    /// The value of the field with this name, or `None` when the record does not carry it.
    pub fn get(&self, name: &str) -> Option<&CapabilityValue> {
        self.fields.iter().find(|(key, _)| key == name).map(|(_, value)| value)
    }

    /// The record's fields, in their own order.
    pub fn fields(&self) -> &[(String, CapabilityValue)] {
        &self.fields
    }

    /// The field names, in their own order.
    pub fn names(&self) -> Vec<&str> {
        self.fields.iter().map(|(key, _)| key.as_str()).collect()
    }

    /// The record without the field named `name`, or the same record when it does not carry it.
    ///
    /// Chainable, and the mirror of [`Self::with`]: it is how a test or a caller builds a record that
    /// is missing an optional field, which is a state the schema allows and the validator must
    /// therefore distinguish from "wrong".
    pub fn without(mut self, name: &str) -> Self {
        self.remove(name);
        self
    }

    /// How many fields the record carries.
    pub fn len(&self) -> usize {
        self.fields.len()
    }

    /// Whether the record carries no fields at all.
    pub fn is_empty(&self) -> bool {
        self.fields.is_empty()
    }

    /// The text of a text field, or an empty string when it is absent or not text.
    ///
    /// Convenience for the overwhelmingly common case (an `id`, a `label`), and deliberately
    /// forgiving in the *reading* direction: a designer listing items should not have to unwrap two
    /// `Option`s to display a name. Writing is where strictness belongs, and there
    /// [`RecordList::validate`] is the gate.
    pub fn text(&self, name: &str) -> &str {
        match self.get(name) {
            Some(CapabilityValue::String(text)) => text.as_str(),
            _ => "",
        }
    }

    /// Removes the field with this name, returning its value.
    fn remove(&mut self, name: &str) -> Option<CapabilityValue> {
        let index = self.fields.iter().position(|(key, _)| key == name)?;
        Some(self.fields.remove(index).1)
    }

    /// Re-inserts a field at `position`, or at the end when `position` is past the end.
    fn insert_at(&mut self, position: usize, name: String, value: CapabilityValue) {
        let position = position.min(self.fields.len());
        self.fields.insert(position, (name, value));
    }
}

/// An ordered list of records that share one declared schema.
///
/// The schema travels **with** the value, so a list is self-describing: [`Self::validate`],
/// [`Self::get_by_id`], [`Self::move_item`] and [`Self::move_field`] need no second argument, and a
/// value that arrived from a document cannot be read with the wrong schema by mistake.
#[derive(Debug, Clone, PartialEq)]
pub struct RecordList {
    schema: &'static RecordSchema,
    items: Vec<RecordValue>,
}

impl RecordList {
    /// An empty list for `schema`.
    pub const fn new(schema: &'static RecordSchema) -> Self {
        Self { schema, items: Vec::new() }
    }

    /// A list holding `items`.
    pub fn from_items(schema: &'static RecordSchema, items: Vec<RecordValue>) -> Self {
        Self { schema, items }
    }

    /// The schema every record in this list is judged against.
    pub const fn schema(&self) -> &'static RecordSchema {
        self.schema
    }

    /// The records, in order.
    pub fn items(&self) -> &[RecordValue] {
        &self.items
    }

    /// The number of records.
    pub fn len(&self) -> usize {
        self.items.len()
    }

    /// Whether the list holds no records at all.
    pub fn is_empty(&self) -> bool {
        self.items.is_empty()
    }

    /// The record at `index`, or `None` when the index is past the end.
    pub fn get(&self, index: usize) -> Option<&RecordValue> {
        self.items.get(index)
    }

    /// The stable id of the record at `index`, or `None` when the index is past the end or the
    /// record's id is absent or not text.
    pub fn id_of(&self, index: usize) -> Option<&str> {
        let record = self.items.get(index)?;
        match record.get(self.schema.id_field) {
            Some(CapabilityValue::String(text)) if !text.is_empty() => Some(text.as_str()),
            _ => None,
        }
    }

    /// The position of the record whose id is `id`.
    ///
    /// Reads through the schema's declared identity field rather than a field named `"id"`, so a
    /// control that keys on `"key"` or `"name"` is served by the same code.
    pub fn index_of_id(&self, id: &str) -> Option<usize> {
        self.items.iter().position(|record| record.text(self.schema.id_field) == id)
    }

    /// The record whose id is `id`, or `None`.
    ///
    /// This is the operation an ordered list needs and a positional read cannot give: a selection, a
    /// scroll anchor or a pending edit survives a reorder only if it is held by id.
    pub fn get_by_id(&self, id: &str) -> Option<&RecordValue> {
        self.items.iter().find(|record| record.text(self.schema.id_field) == id)
    }

    /// Appends one record.
    pub fn push(&mut self, record: RecordValue) {
        self.items.push(record);
    }

    /// Moves the record at `from` so that it sits at position `to`.
    ///
    /// # Why a move and not a remove-then-insert
    ///
    /// A remove-then-insert pair is two mutations: a caller that fails between them has lost a
    /// record, and a reorder expressed as `remove(i); insert(j, ..)` has to get the `j` adjustment
    /// right or it lands one position off. One operation that validates both indices **before**
    /// touching the list cannot do either, and the record it moves is the same record — same id,
    /// same fields — so identity is preserved by construction.
    ///
    /// An out-of-range index is reported rather than clamped: a designer dragging a row past the end
    /// of its list is a bug in the drag, and silently clamping it would apply a reorder nobody
    /// asked for.
    pub fn move_item(&mut self, from: usize, to: usize) -> Result<(), RecordViolation> {
        if from >= self.items.len() {
            return Err(RecordViolation::IndexOutOfRange {
                operation: RecordOperation::MoveItem,
                index: from,
                len: self.items.len(),
            });
        }
        if to >= self.items.len() {
            return Err(RecordViolation::IndexOutOfRange {
                operation: RecordOperation::MoveItem,
                index: to,
                len: self.items.len(),
            });
        }
        if from == to {
            return Ok(());
        }
        let record = self.items.remove(from);
        self.items.insert(to, record);
        Ok(())
    }

    /// Moves the field named `name` so that it sits at position `to` in **every** record that
    /// carries it.
    ///
    /// This is the data side of a designer's column reorder: the columns of the list are the schema's
    /// fields, and the move has to keep the list consistent — a per-record reorder would leave
    /// records whose shared fields disagree about their order, which is exactly the shape
    /// [`Self::validate`] exists to reject.
    ///
    /// A record that does not carry the field (it is optional and unset) is left alone: "move the
    /// field" cannot mean "add it to a record that does not have it". So the invariant is that the
    /// fields the records **do** share keep the same relative order, not that every record ends up
    /// with the same length. Ids are untouched, so identity survives a column move by construction.
    ///
    /// Every decision is made before the first mutation: an undeclared name or an out-of-range target
    /// leaves the list exactly as it was.
    pub fn move_field(&mut self, name: &str, to: usize) -> Result<(), RecordViolation> {
        let Some(source) = self.schema.field_index(name) else {
            return Err(RecordViolation::UnknownFieldName { field: String::from(name) });
        };
        if to >= self.schema.fields.len() {
            return Err(RecordViolation::IndexOutOfRange {
                operation: RecordOperation::MoveField,
                index: to,
                len: self.schema.fields.len(),
            });
        }
        if source == to {
            return Ok(());
        }
        for record in &mut self.items {
            if let Some(value) = record.remove(name) {
                // `insert_at` clamps, so a field moved to the end of a shorter record lands at the
                // end rather than one past it.
                record.insert_at(to, String::from(name), value);
            }
        }
        Ok(())
    }

    /// The same list, judged by `schema` from now on.
    ///
    /// # Why a value needs adopting rather than a schema being compared
    ///
    /// A record list arriving from JSON or the C ABI carries no `&'static` schema — neither boundary
    /// can name one — so the list is built against
    /// [`EMPTY_RECORD_SCHEMA`] and its records are checked against the *property's* schema instead
    /// ([`Self::validate_against`]). A control that wants the identity lookups
    /// ([`Self::get_by_id`], [`Self::index_of_id`]) on such a list adopts it, which is what gives
    /// those methods the identity field to read.
    pub fn with_schema(self, schema: &'static RecordSchema) -> Self {
        Self { schema, items: self.items }
    }

    /// Checks every record against `schema`, returning the first violation.
    ///
    /// This is the entry point for a list that arrived without a schema of its own: the property it is
    /// written to knows its schema, and that is the only declaration the check should use. Judging a
    /// value by a schema it brought with it would let a caller satisfy a rule the control never
    /// published.
    ///
    /// # Why this is a whole-list check and not a per-record one
    ///
    /// Two of the rules are properties of the *list*, not of a record: an id must be unique across
    /// the list, and the schema's own identity field must exist. A per-record validator could not
    /// see either, and a caller that ran one would believe a list with two `"a"` ids was fine.
    ///
    /// # What a caller does with the answer
    ///
    /// Refuse the update and change nothing. Every rule here is checked before the list is handed
    /// over, so an update that fails this leaves the control's own storage untouched — which is what
    /// makes a withdrawn or superseded edit safe to abandon rather than something that has to be
    /// rolled back.
    pub fn validate_against(&self, schema: &'static RecordSchema) -> Result<(), RecordViolation> {
        if schema.id_field_schema().is_none() {
            return Err(RecordViolation::SchemaHasNoIdField { field: schema.id_field });
        }
        for (index, record) in self.items.iter().enumerate() {
            for (name, value) in record.fields() {
                let Some(field) = schema.field(name) else {
                    return Err(RecordViolation::UnknownField { index, field: name.clone() });
                };
                if !value_matches_kind(value, field.kind) {
                    return Err(RecordViolation::WrongKind {
                        index,
                        field: name.clone(),
                        expected: field.kind,
                    });
                }
            }
            for field in schema.fields {
                if field.required && record.get(field.name).is_none() {
                    return Err(RecordViolation::MissingField { index, field: field.name });
                }
            }
            // A single name repeated inside one record: the record has no single value for it, and
            // `get` would answer the first while a writer might set the second.
            if let Some(repeated) = repeated_field(record) {
                return Err(RecordViolation::RepeatedField { index, field: repeated });
            }
        }
        self.check_ids(schema)
    }

    /// Checks every record against the schema the list itself carries.
    ///
    /// A convenience over [`Self::validate_against`] for a list a control built — it already knows
    /// which schema it built it for.
    pub fn validate(&self) -> Result<(), RecordViolation> {
        self.validate_against(self.schema)
    }

    /// The identity rules, which are properties of the list rather than of one record.
    fn check_ids(&self, schema: &'static RecordSchema) -> Result<(), RecordViolation> {
        for (index, record) in self.items.iter().enumerate() {
            let id = record.text(schema.id_field);
            if schema.ids_required && id.is_empty() {
                return Err(RecordViolation::EmptyId { index });
            }
            // Only a non-empty id can collide: two records that both omit the id are "missing",
            // reported above when the schema requires it, and allowed when it does not.
            if !schema.ids_unique || id.is_empty() {
                continue;
            }
            if let Some(first) =
                self.items[..index].iter().position(|earlier| earlier.text(schema.id_field) == id)
            {
                return Err(RecordViolation::DuplicateId {
                    id: String::from(id),
                    first,
                    second: index,
                });
            }
        }
        Ok(())
    }

    /// A copy holding every record except the one whose id is `id`.
    ///
    /// Removal is expressed as a copy rather than a mutation because that is where it is used: a
    /// host handing the library a new list. Returning `None` when the id is absent (rather than an
    /// unchanged copy) keeps "removed" and "was not there" distinguishable at the call site.
    pub fn without_id(&self, id: &str) -> Option<Self> {
        let index = self.index_of_id(id)?;
        let mut items = self.items.clone();
        items.remove(index);
        Some(Self { schema: self.schema, items })
    }
}

impl Default for RecordList {
    /// An empty list for an empty schema.
    ///
    /// `Default` is required by the collection types it sits beside, and the only honest default is
    /// the schema-less one: a list that claims a schema it was not given would validate against
    /// rules nobody declared.
    fn default() -> Self {
        Self::new(&EMPTY_RECORD_SCHEMA)
    }
}

/// The schema a schema-less list carries: no fields, no identity.
///
/// # Why this is not "no schema"
///
/// A list built by the JSON loader or the C ABI cannot name a `&'static` schema, so it carries this
/// one and is judged by [`RecordList::validate_against`] against the schema the *property* declares.
/// Carrying nothing at all would mean the fields could not be enumerated by anyone — a value that
/// describes itself is worth more than one that must be paired with its schema before it can be read.
pub const EMPTY_RECORD_SCHEMA: RecordSchema =
    RecordSchema { fields: &[], id_field: "", ids_unique: false, ids_required: false };

/// The name a record repeats, if it repeats one.
///
/// Returns the *first* repeated name in field order, so the message is deterministic rather than
/// depending on where the scan happens to look first.
fn repeated_field(record: &RecordValue) -> Option<String> {
    let fields = record.fields();
    for (position, (name, _)) in fields.iter().enumerate() {
        if fields[position + 1..].iter().any(|(other, _)| other == name) {
            return Some(name.clone());
        }
    }
    None
}

/// Whether a value's runtime shape satisfies a declared [`PropertyValueKind`].
///
/// Mirrors the property write path's own kind check, stated here so the record validator does not
/// depend on it. `Number` accepts either numeric carrier for the reason it does there: an integer
/// mode and a decimal mode are the same declaration.
fn value_matches_kind(value: &CapabilityValue, kind: PropertyValueKind) -> bool {
    match kind {
        PropertyValueKind::Bool => matches!(value, CapabilityValue::Bool(_)),
        PropertyValueKind::Int => matches!(value, CapabilityValue::Int(_)),
        PropertyValueKind::UInt => matches!(value, CapabilityValue::UInt(_)),
        PropertyValueKind::Float => matches!(value, CapabilityValue::Float(_)),
        PropertyValueKind::Number => {
            matches!(
                value,
                CapabilityValue::Int(_) | CapabilityValue::UInt(_) | CapabilityValue::Float(_)
            )
        }
        PropertyValueKind::String | PropertyValueKind::Enum => {
            matches!(value, CapabilityValue::String(_))
        }
        PropertyValueKind::Color => matches!(value, CapabilityValue::Color(_)),
        PropertyValueKind::Rect => matches!(value, CapabilityValue::Rect(_)),
        PropertyValueKind::IconSpec => matches!(value, CapabilityValue::IconSpec(_)),
        // A field whose declared kind is itself a record list: no control declares one, and
        // accepting any `Records` value here would let an unvalidated nested list through under the
        // name of a field. Refused rather than guessed.
        PropertyValueKind::Records => false,
    }
}

/// Which list edit an index complaint came from.
///
/// Carried so a caller (and the message) can say which operation the index was out of range *for*:
/// a row index and a column index are not interchangeable, and a bare number would not say which
/// the caller got wrong.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RecordOperation {
    /// [`RecordList::move_item`].
    MoveItem,
    /// [`RecordList::move_field`].
    MoveField,
}

impl RecordOperation {
    /// The operation's name, for a message.
    pub const fn token(self) -> &'static str {
        match self {
            Self::MoveItem => "move item",
            Self::MoveField => "move field",
        }
    }
}

/// Why a record list does not conform to its schema, or why an edit on it was refused.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RecordViolation {
    /// The schema names an identity field it does not declare, so nothing can be keyed on it.
    SchemaHasNoIdField {
        /// The field the schema named.
        field: &'static str,
    },
    /// A record is missing a field the schema requires.
    MissingField {
        /// Which record, by position.
        index: usize,
        /// The required field that is absent.
        field: &'static str,
    },
    /// A record carries a field the schema does not declare.
    UnknownField {
        /// Which record, by position.
        index: usize,
        /// The undeclared field.
        field: String,
    },
    /// A record carries the same field twice, so it has no single value for it.
    RepeatedField {
        /// Which record, by position.
        index: usize,
        /// The repeated field.
        field: String,
    },
    /// A field's value is not of the kind the schema declares.
    WrongKind {
        /// Which record, by position.
        index: usize,
        /// The field whose value was wrong.
        field: String,
        /// The kind the schema declares.
        expected: PropertyValueKind,
    },
    /// An id the schema requires is empty.
    EmptyId {
        /// Which record, by position.
        index: usize,
    },
    /// Two records claim the same id, which a unique-keyed schema refuses.
    DuplicateId {
        /// The id both records claim.
        id: String,
        /// The position of the record that claimed it first.
        first: usize,
        /// The position of the record that repeated it.
        second: usize,
    },
    /// An edit named an index past the end of what it was editing.
    IndexOutOfRange {
        /// Which edit.
        operation: RecordOperation,
        /// The index that did not resolve.
        index: usize,
        /// How many positions were available.
        len: usize,
    },
    /// An edit named a field the schema does not declare.
    UnknownFieldName {
        /// The undeclared field.
        field: String,
    },
}

impl RecordViolation {
    /// The record the violation is about, when it is about one.
    ///
    /// `None` for the two whole-list complaints (a schema with no identity field, and an edit that
    /// named an unknown field), which is what lets a designer point at the offending row rather than
    /// at the property as a whole.
    pub const fn index(&self) -> Option<usize> {
        match self {
            Self::MissingField { index, .. }
            | Self::UnknownField { index, .. }
            | Self::RepeatedField { index, .. }
            | Self::WrongKind { index, .. }
            | Self::EmptyId { index } => Some(*index),
            Self::DuplicateId { second, .. } => Some(*second),
            Self::IndexOutOfRange { index, .. } => Some(*index),
            Self::SchemaHasNoIdField { .. } | Self::UnknownFieldName { .. } => None,
        }
    }
}

impl core::fmt::Display for RecordViolation {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        match self {
            Self::SchemaHasNoIdField { field } => write!(
                f,
                "the record schema names `{field}` as its identity field but does not declare it"
            ),
            Self::MissingField { index, field } => {
                write!(f, "record {index} is missing the required field `{field}`")
            }
            Self::UnknownField { index, field } => {
                write!(f, "record {index} carries the undeclared field `{field}`")
            }
            Self::RepeatedField { index, field } => {
                write!(f, "record {index} carries the field `{field}` more than once")
            }
            Self::WrongKind { index, field, expected } => write!(
                f,
                "record {index} field `{field}` is not the declared kind `{}`",
                super::designer_manifest::value_kind_token(*expected)
            ),
            Self::EmptyId { index } => {
                write!(f, "record {index} has an empty id, which the schema requires")
            }
            Self::DuplicateId { id, first, second } => write!(
                f,
                "records {first} and {second} claim the same id `{id}`, which the schema requires to \
                 be unique"
            ),
            Self::IndexOutOfRange { operation, index, len } => {
                write!(f, "cannot {} to {index}: the list has {len} positions", operation.token())
            }
            Self::UnknownFieldName { field } => {
                write!(f, "the schema does not declare a field named `{field}`")
            }
        }
    }
}

/// The value a `Records` property carries, or `None` when it carries something else.
///
/// A free function rather than a `CapabilityValue` method so the two error-shaped readers
/// (`as_str`, `as_i64`, …) and this one stay in the same file as the enum they read.
pub fn as_record_list(value: &CapabilityValue) -> Option<&RecordList> {
    match value {
        CapabilityValue::Records(list) => Some(list),
        _ => None,
    }
}

/// The compact description of a record list, for a diagnostic that must fit on one line.
pub fn describe_records(list: &RecordList) -> String {
    format!("{} record(s) of schema {}", list.len(), list.schema().names().join(","))
}

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

    const CHIP_ITEM: RecordSchema = RecordSchema {
        fields: &[
            RecordField::required("id", PropertyValueKind::String),
            RecordField::required("label", PropertyValueKind::String),
            RecordField::optional("selected", PropertyValueKind::Bool),
        ],
        id_field: "id",
        ids_unique: true,
        ids_required: true,
    };

    fn text(value: &str) -> CapabilityValue {
        CapabilityValue::String(String::from(value))
    }

    fn item(id: &str, label: &str, selected: bool) -> RecordValue {
        RecordValue::new()
            .with("id", text(id))
            .with("label", text(label))
            .with("selected", CapabilityValue::Bool(selected))
    }

    fn ids(list: &RecordList) -> Vec<String> {
        (0..list.len())
            .map(|index| String::from(list.id_of(index).expect("every item has an id")))
            .collect()
    }

    fn list() -> RecordList {
        RecordList::from_items(
            &CHIP_ITEM,
            vec![item("a", "Alpha", false), item("b", "Beta", true), item("c", "Gamma", false)],
        )
    }

    /// A well-formed list validates, and every declared field is addressable.
    #[test]
    fn a_conforming_list_validates() {
        let list = list();
        assert_eq!(list.validate(), Ok(()));
        assert_eq!(list.len(), 3);
        assert_eq!(list.schema().names(), vec!["id", "label", "selected"]);
        assert_eq!(list.id_of(1), Some("b"));
        assert_eq!(list.index_of_id("c"), Some(2));
        assert_eq!(list.id_of(3), None);
    }

    /// A move reorders the list **and keeps every item's identity**.
    ///
    /// This is the whole point of declaring an identity field: positions change, the items do not.
    #[test]
    fn a_move_reorders_without_changing_identity() {
        let mut list = list();
        let before = ids(&list);

        list.move_item(0, 2).expect("both indices are in range");

        assert_eq!(list.id_of(0), Some("b"));
        assert_eq!(list.id_of(1), Some("c"));
        assert_eq!(list.id_of(2), Some("a"));
        // The moved record is the *same* record: its own fields travelled with it.
        assert_eq!(list.get_by_id("a").expect("still present").text("label"), "Alpha");
        assert!(matches!(
            list.get_by_id("b").expect("still present").get("selected"),
            Some(CapabilityValue::Bool(true))
        ));
        // And no id was invented or lost by the reorder.
        let mut after = ids(&list);
        let mut sorted_before = before;
        after.sort();
        sorted_before.sort();
        assert_eq!(after, sorted_before);
    }

    /// A move to the same position is a no-op, and an out-of-range index is refused *without*
    /// touching the list.
    #[test]
    fn a_move_that_cannot_happen_leaves_the_list_alone() {
        let mut list = list();
        let unchanged = list.clone();

        assert_eq!(list.move_item(1, 1), Ok(()));
        assert_eq!(list, unchanged);

        assert_eq!(
            list.move_item(3, 0),
            Err(RecordViolation::IndexOutOfRange {
                operation: RecordOperation::MoveItem,
                index: 3,
                len: 3
            })
        );
        assert_eq!(list, unchanged);
        assert_eq!(
            list.move_item(0, 9),
            Err(RecordViolation::IndexOutOfRange {
                operation: RecordOperation::MoveItem,
                index: 9,
                len: 3
            })
        );
        assert_eq!(list, unchanged);
    }

    /// A column move reorders one field in every record that carries it, and leaves ids alone.
    #[test]
    fn a_column_move_reorders_every_record_and_keeps_the_ids_stable() {
        let mut list = list();
        let before = ids(&list);

        // `label` to the front: the order becomes label, id, selected.
        list.move_field("label", 0).expect("label is declared and 0 is in range");

        for record in list.items() {
            assert_eq!(record.names(), vec!["label", "id", "selected"]);
        }
        assert_eq!(list.get(0).expect("first record").text("label"), "Alpha");
        assert_eq!(list.get(0).expect("first record").text("id"), "a");
        assert_eq!(ids(&list), before);
        // A permuted record is still a conforming record.
        assert_eq!(list.validate(), Ok(()));
    }

    /// A record that does not carry the moved field is left alone, and the rest stay consistently
    /// ordered.
    #[test]
    fn a_column_move_skips_a_record_that_does_not_carry_the_field() {
        let mut list = RecordList::from_items(
            &CHIP_ITEM,
            vec![item("a", "Alpha", true), item("b", "Beta", false).without("selected")],
        );

        list.move_field("id", 2).expect("id is declared and 2 is in range");

        assert_eq!(list.get(0).expect("first").names(), vec!["label", "selected", "id"]);
        // The second record has no `selected`, so it keeps the two fields it does have, in the
        // schema-consistent order the move implies.
        assert_eq!(list.get(1).expect("second").names(), vec!["label", "id"]);
        assert_eq!(ids(&list), vec![String::from("a"), String::from("b")]);
        assert_eq!(list.validate(), Ok(()));
    }

    /// A column move that names an undeclared field, or a position past the end, changes nothing.
    #[test]
    fn a_column_move_that_cannot_happen_leaves_the_list_alone() {
        let mut list = list();
        let unchanged = list.clone();

        assert_eq!(
            list.move_field("roles", 0),
            Err(RecordViolation::UnknownFieldName { field: String::from("roles") })
        );
        assert_eq!(list, unchanged);

        assert_eq!(
            list.move_field("label", 3),
            Err(RecordViolation::IndexOutOfRange {
                operation: RecordOperation::MoveField,
                index: 3,
                len: 3
            })
        );
        assert_eq!(list, unchanged);
    }

    /// Each schema rule is enforced, and the violation says which record and which field.
    #[test]
    fn every_schema_rule_is_reported_with_its_record_and_field() {
        // A required field that is absent.
        let missing =
            RecordList::from_items(&CHIP_ITEM, vec![item("a", "A", false).without("label")]);
        assert_eq!(
            missing.validate(),
            Err(RecordViolation::MissingField { index: 0, field: "label" })
        );

        // A field the schema does not declare.
        let unknown =
            RecordList::from_items(&CHIP_ITEM, vec![item("a", "A", false).with("rol", text("x"))]);
        assert_eq!(
            unknown.validate(),
            Err(RecordViolation::UnknownField { index: 0, field: String::from("rol") })
        );

        // A field whose value is not the declared kind.
        let wrong_kind = RecordList::from_items(
            &CHIP_ITEM,
            vec![item("a", "A", false).with("selected", CapabilityValue::UInt(1))],
        );
        assert_eq!(
            wrong_kind.validate(),
            Err(RecordViolation::WrongKind {
                index: 0,
                field: String::from("selected"),
                expected: PropertyValueKind::Bool
            })
        );

        // An empty id where the schema requires one.
        let empty_id = RecordList::from_items(
            &CHIP_ITEM,
            vec![RecordValue::new().with("id", text("")).with("label", text("A"))],
        );
        assert_eq!(empty_id.validate(), Err(RecordViolation::EmptyId { index: 0 }));

        // Two records claiming one id.
        let duplicated =
            RecordList::from_items(&CHIP_ITEM, vec![item("a", "A", false), item("a", "B", false)]);
        assert_eq!(
            duplicated.validate(),
            Err(RecordViolation::DuplicateId { id: String::from("a"), first: 0, second: 1 })
        );

        // The same field twice in one record.
        let repeated = RecordList::from_items(
            &CHIP_ITEM,
            vec![item("a", "A", false).with("label", text("B"))],
        );
        assert_eq!(
            repeated.validate(),
            Err(RecordViolation::RepeatedField { index: 0, field: String::from("label") })
        );
    }

    /// A schema that names an identity field it does not declare is a schema defect, not a list that
    /// quietly cannot be keyed.
    #[test]
    fn a_schema_without_its_own_identity_field_is_refused() {
        const BROKEN: RecordSchema = RecordSchema {
            fields: &[RecordField::required("label", PropertyValueKind::String)],
            id_field: "id",
            ids_unique: true,
            ids_required: true,
        };
        let list =
            RecordList::from_items(&BROKEN, vec![RecordValue::new().with("label", text("A"))]);
        assert_eq!(list.validate(), Err(RecordViolation::SchemaHasNoIdField { field: "id" }));
    }

    /// A schema that does not require unique ids accepts a repeat, and a repeat is still addressable
    /// by position.
    #[test]
    fn a_repeated_id_is_accepted_when_the_schema_allows_it() {
        const ALLOWING: RecordSchema = RecordSchema {
            fields: &[RecordField::required("id", PropertyValueKind::String)],
            id_field: "id",
            ids_unique: false,
            ids_required: true,
        };
        let list = RecordList::from_items(
            &ALLOWING,
            vec![
                RecordValue::new().with("id", text("a")),
                RecordValue::new().with("id", text("a")),
            ],
        );
        assert_eq!(list.validate(), Ok(()));
        assert_eq!(list.index_of_id("a"), Some(0));
    }

    /// An optional field may be absent, and a list is empty only when it holds nothing.
    #[test]
    fn an_optional_field_may_be_absent() {
        let list =
            RecordList::from_items(&CHIP_ITEM, vec![item("a", "A", false).without("selected")]);
        assert_eq!(list.validate(), Ok(()));
        assert!(list.get(0).expect("record").get("selected").is_none());
        assert_eq!(list.get(0).expect("record").text("selected"), "");
        assert!(!list.is_empty());
        assert!(RecordList::new(&CHIP_ITEM).is_empty());
        assert_eq!(list.get(7), None);
    }

    /// Removal is by identity, and an absent id is distinguishable from a removed one.
    #[test]
    fn removal_is_by_identity() {
        let list = list();
        let without_b = list.without_id("b").expect("b is present");
        assert_eq!(without_b.len(), 2);
        assert_eq!(without_b.index_of_id("b"), None);
        // The original is untouched: `without_id` copies.
        assert_eq!(list.len(), 3);
        assert_eq!(list.without_id("zzz"), None);
    }

    /// Every violation renders a message that names the record and the field, so a report can point
    /// at the offending row — and the two whole-list complaints point at no row.
    #[test]
    fn violations_describe_themselves() {
        for violation in [
            RecordViolation::SchemaHasNoIdField { field: "id" },
            RecordViolation::MissingField { index: 2, field: "label" },
            RecordViolation::UnknownField { index: 1, field: String::from("rol") },
            RecordViolation::RepeatedField { index: 0, field: String::from("id") },
            RecordViolation::WrongKind {
                index: 3,
                field: String::from("selected"),
                expected: PropertyValueKind::Bool,
            },
            RecordViolation::EmptyId { index: 0 },
            RecordViolation::DuplicateId { id: String::from("a"), first: 0, second: 4 },
            RecordViolation::IndexOutOfRange {
                operation: RecordOperation::MoveItem,
                index: 9,
                len: 3,
            },
            RecordViolation::UnknownFieldName { field: String::from("roles") },
        ] {
            let rendered = format!("{violation}");
            assert!(!rendered.is_empty(), "{violation:?} has no message");
        }
        assert_eq!(RecordViolation::MissingField { index: 2, field: "label" }.index(), Some(2));
        assert_eq!(
            RecordViolation::DuplicateId { id: String::from("a"), first: 0, second: 4 }.index(),
            Some(4),
            "a duplicate id points at the record that repeated it"
        );
        assert_eq!(
            RecordViolation::SchemaHasNoIdField { field: "id" }.index(),
            None,
            "a schema defect is not a complaint about one row"
        );
    }

    /// A `Records` value is readable as records and as nothing else, and describes itself.
    #[test]
    fn the_accessor_reads_only_a_record_list() {
        let value = CapabilityValue::Records(list());
        assert_eq!(as_record_list(&value).expect("a record list").len(), 3);
        assert_eq!(as_record_list(&CapabilityValue::Null), None);
        assert_eq!(
            describe_records(as_record_list(&value).expect("list")),
            "3 record(s) of schema id,label,selected"
        );
    }

    /// The default list claims no schema, so it accepts only the empty item set.
    #[test]
    fn a_default_list_claims_no_schema() {
        let list = RecordList::default();
        assert!(list.is_empty());
        assert_eq!(list.validate(), Err(RecordViolation::SchemaHasNoIdField { field: "" }));
    }

    /// A field declared as a record list is refused as a record field: no control declares one, and
    /// accepting any list would let an unvalidated nested list through under a field's name.
    #[test]
    fn a_nested_record_list_is_not_a_valid_field_value() {
        let value = CapabilityValue::Records(list());
        assert!(!value_matches_kind(&value, PropertyValueKind::Records));
        assert!(value_matches_kind(&CapabilityValue::Bool(true), PropertyValueKind::Bool));
    }
}