1use blockworx_doc::{
5 block_model::{
6 AreaUpdate, BlockUpdate, Label, LabelUpdate, Pin, PinUpdate, RouteUpdate, TextUpdate,
7 },
8 commit::CommitBuilder,
9 document::Document,
10 geometry::{FracVal, GridRect, GridSize},
11 id::{AreaId, BlockId, PinId, RouteId, RouteLabelId, TextId},
12 opcode::{Crud, OpCodes},
13 values::{LabelSide, PinDir},
14};
15
16use blockworx_store::doc::Writability;
17
18use crate::edit::lock::MaterialPin;
19use crate::edit::lower::role_from_accent;
20
21#[derive(Clone, Copy, Debug)]
23pub enum TitleTarget {
24 Block(BlockId),
25 Area(AreaId),
26}
27
28pub fn rename_title(doc: &Document, target: TitleTarget, name: &str, builder: &mut CommitBuilder) {
31 match target {
32 TitleTarget::Block(id) => {
33 let Some(live) = doc.block(&id) else {
34 return;
35 };
36 if live.title.name == name {
37 return;
38 }
39 builder.push(OpCodes::Block(
40 id,
41 Crud::Update(BlockUpdate::Title(LabelUpdate::Name(name.into()))),
42 ));
43 }
44 TitleTarget::Area(id) => {
45 let Some(live) = doc.area(&id) else {
46 return;
47 };
48 if live.title.name == name {
49 return;
50 }
51 builder.push(OpCodes::Area(
52 id,
53 Crud::Update(AreaUpdate::Title(LabelUpdate::Name(name.into()))),
54 ));
55 }
56 }
57}
58
59#[derive(Clone, Copy, Debug)]
63pub enum AccentTarget {
64 Block(BlockId),
65 Route(RouteId),
66 Area(AreaId),
67 Text(TextId),
68 Port(PinId),
69}
70
71pub fn set_accent(
74 doc: &Document,
75 target: AccentTarget,
76 accent: Option<u8>,
77 builder: &mut CommitBuilder,
78) {
79 let role = role_from_accent(accent);
80 match target {
81 AccentTarget::Block(id) => {
82 let Some(live) = doc.block(&id) else {
83 return;
84 };
85 if live.role == role {
86 return;
87 }
88 builder.push(OpCodes::Block(id, Crud::Update(BlockUpdate::Role(role))));
89 }
90 AccentTarget::Route(id) => {
91 let Some(live) = doc.route(&id) else {
92 return;
93 };
94 if live.role == role {
95 return;
96 }
97 builder.push(OpCodes::Route(id, Crud::Update(RouteUpdate::Role(role))));
98 }
99 AccentTarget::Area(id) => {
100 let Some(live) = doc.area(&id) else {
101 return;
102 };
103 if live.role == role {
104 return;
105 }
106 builder.push(OpCodes::Area(id, Crud::Update(AreaUpdate::Role(role))));
107 }
108 AccentTarget::Text(id) => {
109 let Some(live) = doc.text(&id) else {
110 return;
111 };
112 if live.role == role {
113 return;
114 }
115 builder.push(OpCodes::Text(id, Crud::Update(TextUpdate::Role(role))));
116 }
117 AccentTarget::Port(id) => {
118 let Some(live) = doc.pin(&id) else {
119 return;
120 };
121 if live.port_accent == role {
122 return;
123 }
124 builder.push(OpCodes::Pin(id, Crud::Update(PinUpdate::PortAccent(role))));
125 }
126 }
127}
128
129pub fn rename_type(doc: &Document, block: BlockId, name: &str, builder: &mut CommitBuilder) {
131 let Some(live) = doc.block(&block) else {
132 return;
133 };
134 if live.type_label.name == name {
135 return;
136 }
137 builder.push(OpCodes::Block(
138 block,
139 Crud::Update(BlockUpdate::TypeLabel(LabelUpdate::Name(name.into()))),
140 ));
141}
142
143#[derive(Clone, Copy, Debug, PartialEq, Eq)]
146pub struct LabelFitWidth(u32);
147
148impl LabelFitWidth {
149 pub fn new(cells: u32) -> Self {
150 Self(cells)
151 }
152}
153
154impl From<LabelFitWidth> for u32 {
155 fn from(width: LabelFitWidth) -> u32 {
156 width.0
157 }
158}
159
160fn widened(pin: &Pin, fit: LabelFitWidth) -> Option<GridRect> {
163 let rect = pin.rect;
164 let fit = u32::from(fit);
165 (fit > rect.size.w).then_some(GridRect {
166 size: GridSize {
167 w: fit,
168 ..rect.size
169 },
170 ..rect
171 })
172}
173
174pub fn rename_pin(
176 doc: &Document,
177 target: MaterialPin,
178 name: &str,
179 fit: LabelFitWidth,
180 builder: &mut CommitBuilder,
181) {
182 let pin = target.id();
183 let Some(live) = doc.pin(&pin) else {
184 return;
185 };
186 if live.name != name {
187 builder.push(OpCodes::Pin(
188 pin,
189 Crud::Update(PinUpdate::Name(name.into())),
190 ));
191 }
192 if let Some(rect) = widened(live, fit) {
193 builder.push(OpCodes::Pin(pin, Crud::Update(PinUpdate::Rect(rect))));
194 }
195}
196
197pub fn set_tag(doc: &Document, target: MaterialPin, tag: &str, builder: &mut CommitBuilder) {
200 let pin = target.id();
201 let Some(live) = doc.pin(&pin) else {
202 return;
203 };
204 if live.tag == tag {
205 return;
206 }
207 builder.push(OpCodes::Pin(pin, Crud::Update(PinUpdate::Tag(tag.into()))));
208}
209
210pub fn retype_pin(
213 doc: &Document,
214 target: MaterialPin,
215 type_name: &str,
216 fit: LabelFitWidth,
217 builder: &mut CommitBuilder,
218) {
219 let pin = target.id();
220 let Some(live) = doc.pin(&pin) else {
221 return;
222 };
223 if live.type_name != type_name {
224 builder.push(OpCodes::Pin(
225 pin,
226 Crud::Update(PinUpdate::TypeName(type_name.into())),
227 ));
228 }
229 if let Some(rect) = widened(live, fit) {
230 builder.push(OpCodes::Pin(pin, Crud::Update(PinUpdate::Rect(rect))));
231 }
232}
233
234fn cycled(dir: PinDir) -> PinDir {
236 match dir {
237 PinDir::InOut => PinDir::Input,
238 PinDir::Input => PinDir::Output,
239 PinDir::Output => PinDir::InOut,
240 }
241}
242
243pub fn cycle_dir(doc: &Document, target: MaterialPin, builder: &mut CommitBuilder) {
246 let pin = target.id();
247 let Some(live) = doc.pin(&pin) else {
248 return;
249 };
250 builder.push(OpCodes::Pin(
251 pin,
252 Crud::Update(PinUpdate::Dir(cycled(live.dir))),
253 ));
254}
255
256pub fn set_dirs(doc: &Document, pins: &[MaterialPin], dir: PinDir, builder: &mut CommitBuilder) {
259 for target in pins {
260 let id = target.id();
261 let Some(live) = doc.pin(&id) else {
262 continue;
263 };
264 if live.dir == dir {
265 continue;
266 }
267 builder.push(OpCodes::Pin(id, Crud::Update(PinUpdate::Dir(dir))));
268 }
269}
270
271#[derive(Clone, Copy, Debug, PartialEq, Eq)]
274pub enum TagVisibility {
275 Shown,
276 Hidden,
277}
278
279impl TagVisibility {
280 pub fn is_hidden(self) -> bool {
281 self == TagVisibility::Hidden
282 }
283}
284
285impl From<bool> for TagVisibility {
286 fn from(hidden: bool) -> Self {
287 if hidden {
288 TagVisibility::Hidden
289 } else {
290 TagVisibility::Shown
291 }
292 }
293}
294
295pub fn set_tag_visibility(
298 doc: &Document,
299 pins: &[PinId],
300 visibility: TagVisibility,
301 builder: &mut CommitBuilder,
302) {
303 let hidden = visibility.is_hidden();
304 for id in pins {
305 let Some(live) = doc.pin(id) else {
306 continue;
307 };
308 if live.tag_hidden == hidden {
309 continue;
310 }
311 builder.push(OpCodes::Pin(
312 *id,
313 Crud::Update(PinUpdate::TagHidden(hidden)),
314 ));
315 }
316}
317
318#[derive(Clone, Copy, Debug, PartialEq, Eq)]
321pub enum InterfaceLock {
322 Locked,
323 Unlocked,
324}
325
326impl InterfaceLock {
327 pub fn is_locked(self) -> bool {
328 self == InterfaceLock::Locked
329 }
330}
331
332impl From<bool> for InterfaceLock {
333 fn from(locked: bool) -> Self {
334 if locked {
335 InterfaceLock::Locked
336 } else {
337 InterfaceLock::Unlocked
338 }
339 }
340}
341
342#[derive(Clone, Copy, PartialEq, Eq, Debug)]
353pub enum Authoring {
354 Offered,
355 Withheld,
356}
357
358impl Authoring {
359 pub fn of(writability: Writability, lock: InterfaceLock) -> Self {
362 match (writability, lock.is_locked()) {
363 (Writability::Writable, false) => Authoring::Offered,
364 _ => Authoring::Withheld,
365 }
366 }
367
368 pub fn is_withheld(self) -> bool {
369 self == Authoring::Withheld
370 }
371}
372
373impl From<Writability> for Authoring {
374 fn from(writability: Writability) -> Self {
375 Authoring::of(writability, InterfaceLock::Unlocked)
376 }
377}
378
379pub fn set_locked(
382 doc: &Document,
383 block: BlockId,
384 lock: InterfaceLock,
385 builder: &mut CommitBuilder,
386) {
387 let Some(live) = doc.block(&block) else {
388 return;
389 };
390 let locked = lock.is_locked();
391 if live.locked == locked {
392 return;
393 }
394 builder.push(OpCodes::Block(
395 block,
396 Crud::Update(BlockUpdate::Locked(locked)),
397 ));
398}
399
400pub fn rename_route(doc: &Document, route: RouteId, name: &str, builder: &mut CommitBuilder) {
403 if name.trim().is_empty() {
404 return;
405 }
406 let Some(live) = doc.route(&route) else {
407 return;
408 };
409 if live.name == name {
410 return;
411 }
412 builder.push(OpCodes::Route(
413 route,
414 Crud::Update(RouteUpdate::Name(name.into())),
415 ));
416}
417
418#[derive(Clone, Copy, Debug)]
420pub enum LabelTarget {
421 BlockTitle(BlockId),
422 BlockType(BlockId),
423 AreaTitle(AreaId),
424}
425
426#[derive(Clone, Copy, Debug)]
429pub struct LabelPlacement {
430 pub offset: FracVal,
431 pub side: Option<LabelSide>,
432}
433
434fn placement_updates(label: &Label, placement: LabelPlacement) -> Vec<LabelUpdate> {
437 let mut updates = Vec::new();
438 if label.offset != placement.offset {
439 updates.push(LabelUpdate::Offset(placement.offset));
440 }
441 if let Some(side) = placement.side.filter(|side| *side != label.side) {
442 updates.push(LabelUpdate::Side(side));
443 }
444 updates
445}
446
447pub fn place_label(
450 doc: &Document,
451 target: LabelTarget,
452 placement: LabelPlacement,
453 builder: &mut CommitBuilder,
454) {
455 match target {
456 LabelTarget::BlockTitle(id) => {
457 let Some(live) = doc.block(&id) else {
458 return;
459 };
460 builder.extend(
461 placement_updates(&live.title, placement)
462 .into_iter()
463 .map(|update| OpCodes::Block(id, Crud::Update(BlockUpdate::Title(update)))),
464 );
465 }
466 LabelTarget::BlockType(id) => {
467 let Some(live) = doc.block(&id) else {
468 return;
469 };
470 builder.extend(
471 placement_updates(&live.type_label, placement)
472 .into_iter()
473 .map(|update| OpCodes::Block(id, Crud::Update(BlockUpdate::TypeLabel(update)))),
474 );
475 }
476 LabelTarget::AreaTitle(id) => {
477 let Some(live) = doc.area(&id) else {
478 return;
479 };
480 builder.extend(
481 placement_updates(&live.title, placement)
482 .into_iter()
483 .map(|update| OpCodes::Area(id, Crud::Update(AreaUpdate::Title(update)))),
484 );
485 }
486 }
487}
488
489pub fn edit_text(doc: &Document, text: TextId, content: &str, builder: &mut CommitBuilder) {
493 let Some(live) = doc.text(&text) else {
494 return;
495 };
496 if content.trim().is_empty() {
497 builder.push(OpCodes::Text(text, Crud::Delete));
498 return;
499 }
500 if live.text == content {
501 return;
502 }
503 builder.push(OpCodes::Text(
504 text,
505 Crud::Update(TextUpdate::Text(content.into())),
506 ));
507}
508
509pub fn clear_wire_label(doc: &Document, label: RouteLabelId, builder: &mut CommitBuilder) {
512 let Some(live) = doc.route_label(&label) else {
513 return;
514 };
515 let route = live.owner;
516 builder.push(OpCodes::RouteLabel(label, Crud::Delete));
517 if doc.route(&route).is_some_and(|r| !r.name.is_empty()) {
518 builder.push(OpCodes::Route(
519 route,
520 Crud::Update(RouteUpdate::Name(String::new())),
521 ));
522 }
523}
524
525#[cfg(test)]
526mod tests {
527 use super::*;
528
529 fn material(doc: &Document, n: u32) -> MaterialPin {
533 MaterialPin::of(doc, pin_id(n)).expect("the pin's owner is unlocked")
534 }
535 use crate::edit::harness::{fold, route_label_create, seals_to_nothing, wired};
536 use blockworx_doc::fixtures::{area_id, block_id, pin_id, route_id, route_label_id, text_id};
537 use blockworx_doc::values::Role;
538
539 #[test]
540 fn rename_title_lands_on_a_block_and_an_area() {
541 let doc = wired();
542 assert_eq!(
543 doc.block(&block_id(1)).unwrap().title.name,
544 "b1",
545 "precondition: the scene's block starts under its fixture name"
546 );
547
548 let mut builder = CommitBuilder::new("Renamed a block");
549 rename_title(&doc, TitleTarget::Block(block_id(1)), "ALU", &mut builder);
550 let doc = fold(builder, &doc);
551 assert_eq!(doc.block(&block_id(1)).unwrap().title.name, "ALU");
552
553 let mut builder = CommitBuilder::new("Renamed an area");
554 rename_title(&doc, TitleTarget::Area(area_id(8)), "notes", &mut builder);
555 let doc = fold(builder, &doc);
556 assert_eq!(doc.area(&area_id(8)).unwrap().title.name, "notes");
557 }
558
559 #[test]
560 fn renaming_to_the_same_name_seals_to_nothing() {
561 let doc = wired();
562 let mut builder = CommitBuilder::new("Renamed a block");
563 rename_title(&doc, TitleTarget::Block(block_id(1)), "b1", &mut builder);
564 seals_to_nothing(builder);
565 }
566
567 #[test]
568 fn an_absent_target_pushes_nothing() {
569 let doc = wired();
570 let mut builder = CommitBuilder::new("Renamed a ghost");
571 rename_title(&doc, TitleTarget::Block(block_id(99)), "x", &mut builder);
572 seals_to_nothing(builder);
573 }
574
575 #[test]
576 fn set_accent_recolors_every_target_kind() {
577 let doc = wired();
578 let mut builder = CommitBuilder::new("Recolored the scene");
579 set_accent(
580 &doc,
581 AccentTarget::Block(block_id(1)),
582 Some(0),
583 &mut builder,
584 );
585 set_accent(
586 &doc,
587 AccentTarget::Route(route_id(5)),
588 Some(3),
589 &mut builder,
590 );
591 set_accent(&doc, AccentTarget::Port(pin_id(3)), Some(7), &mut builder);
592 let doc = fold(builder, &doc);
593 assert_eq!(doc.block(&block_id(1)).unwrap().role, Role::Accent1);
594 assert_eq!(doc.route(&route_id(5)).unwrap().role, Role::Accent4);
595 assert_eq!(doc.pin(&pin_id(3)).unwrap().port_accent, Role::Accent8);
596
597 let mut builder = CommitBuilder::new("Cleared an accent");
600 set_accent(&doc, AccentTarget::Block(block_id(1)), None, &mut builder);
601 let doc = fold(builder, &doc);
602 assert_eq!(doc.block(&block_id(1)).unwrap().role, Role::Accent0);
603 let mut builder = CommitBuilder::new("Cleared it again");
604 set_accent(&doc, AccentTarget::Block(block_id(1)), None, &mut builder);
605 seals_to_nothing(builder);
606 }
607
608 fn pin(doc: &Document, id: u32) -> &Pin {
609 doc.pin(&pin_id(id)).expect("the scene's pin exists")
610 }
611
612 fn body_width(doc: &Document, id: u32) -> u32 {
613 pin(doc, id).rect.size.w
614 }
615
616 fn locked_scene() -> Document {
619 let doc = wired();
620 let mut builder = CommitBuilder::new("Locked a block");
621 set_locked(&doc, block_id(1), InterfaceLock::Locked, &mut builder);
622 let doc = fold(builder, &doc);
623 assert!(
624 doc.block(&block_id(1))
625 .expect("the scene's block exists")
626 .locked,
627 "precondition: the owner is locked"
628 );
629 assert_eq!(
630 pin(&doc, 3).owner,
631 block_id(1),
632 "precondition: pin 3 belongs to the locked block"
633 );
634 doc
635 }
636
637 #[test]
638 fn rename_type_writes_the_type_label_and_leaves_the_title() {
639 let doc = wired();
640 assert_eq!(
641 doc.block(&block_id(1))
642 .expect("the scene's block exists")
643 .type_label
644 .name,
645 "",
646 "precondition: the scene's block carries no type label"
647 );
648
649 let mut builder = CommitBuilder::new("Retyped a block");
650 rename_type(&doc, block_id(1), "ALU", &mut builder);
651 let doc = fold(builder, &doc);
652 let block = doc.block(&block_id(1)).expect("the scene's block exists");
653 assert_eq!(block.type_label.name, "ALU");
654 assert_eq!(block.title.name, "b1", "the twin label is untouched");
655
656 let mut builder = CommitBuilder::new("Retyped it the same");
657 rename_type(&doc, block_id(1), "ALU", &mut builder);
658 seals_to_nothing(builder);
659 }
660
661 #[test]
662 fn rename_pin_writes_the_name_and_widens_the_body() {
663 let doc = wired();
664 assert_eq!(
665 body_width(&doc, 3),
666 0,
667 "precondition: the fixture pin's body starts unsized"
668 );
669
670 let mut builder = CommitBuilder::new("Renamed a pin");
671 rename_pin(
672 &doc,
673 material(&doc, 3),
674 "clk",
675 LabelFitWidth::new(5),
676 &mut builder,
677 );
678 let doc = fold(builder, &doc);
679 assert_eq!(pin(&doc, 3).name, "clk");
680 assert_eq!(body_width(&doc, 3), 5);
681
682 let mut builder = CommitBuilder::new("Renamed it shorter");
683 rename_pin(
684 &doc,
685 material(&doc, 3),
686 "d0",
687 LabelFitWidth::new(3),
688 &mut builder,
689 );
690 let doc = fold(builder, &doc);
691 assert_eq!(pin(&doc, 3).name, "d0");
692 assert_eq!(body_width(&doc, 3), 5, "a widened body is never narrowed");
693
694 let mut builder = CommitBuilder::new("Remeasured the same name");
697 rename_pin(
698 &doc,
699 material(&doc, 3),
700 "d0",
701 LabelFitWidth::new(7),
702 &mut builder,
703 );
704 let doc = fold(builder, &doc);
705 assert_eq!(pin(&doc, 3).name, "d0");
706 assert_eq!(body_width(&doc, 3), 7);
707
708 let mut builder = CommitBuilder::new("Renamed it to itself");
709 rename_pin(
710 &doc,
711 material(&doc, 3),
712 "d0",
713 LabelFitWidth::new(7),
714 &mut builder,
715 );
716 seals_to_nothing(builder);
717 }
718
719 #[test]
720 fn retype_pin_writes_the_type_line_and_widens_the_body() {
721 let doc = wired();
722 assert_eq!(
723 pin(&doc, 3).type_name,
724 "",
725 "precondition: the fixture pin carries no type line"
726 );
727
728 let mut builder = CommitBuilder::new("Retyped a pin");
729 retype_pin(
730 &doc,
731 material(&doc, 3),
732 "clock",
733 LabelFitWidth::new(6),
734 &mut builder,
735 );
736 let doc = fold(builder, &doc);
737 assert_eq!(pin(&doc, 3).type_name, "clock");
738 assert_eq!(body_width(&doc, 3), 6);
739
740 let mut builder = CommitBuilder::new("Retyped it the same");
741 retype_pin(
742 &doc,
743 material(&doc, 3),
744 "clock",
745 LabelFitWidth::new(6),
746 &mut builder,
747 );
748 seals_to_nothing(builder);
749 }
750
751 #[test]
752 fn set_tag_writes_the_location_designator() {
753 let doc = wired();
754 assert_eq!(
755 pin(&doc, 3).tag,
756 "",
757 "precondition: the fixture pin is untagged"
758 );
759
760 let mut builder = CommitBuilder::new("Tagged a pin");
761 set_tag(&doc, material(&doc, 3), "U3", &mut builder);
762 let doc = fold(builder, &doc);
763 assert_eq!(pin(&doc, 3).tag, "U3");
764 assert_eq!(
765 body_width(&doc, 3),
766 0,
767 "tags extend over the stub, so the body is not widened"
768 );
769
770 let mut builder = CommitBuilder::new("Tagged it the same");
771 set_tag(&doc, material(&doc, 3), "U3", &mut builder);
772 seals_to_nothing(builder);
773 }
774
775 #[test]
779 fn a_locked_owner_refuses_the_capability_material_edits_need() {
780 let doc = locked_scene();
781 for n in [3u32, 4] {
782 assert_eq!(
783 MaterialPin::of(&doc, pin_id(n)),
784 None,
785 "pin {n} belongs to a locked block, so no material edit can name it",
786 );
787 }
788 assert!(
789 MaterialPin::all(&doc, &[pin_id(3), pin_id(4)]).is_empty(),
790 "and a group of them is an empty set, not a refused edit",
791 );
792
793 let mut builder = CommitBuilder::new("Redirected frozen pins");
796 set_dirs(
797 &doc,
798 &MaterialPin::all(&doc, &[pin_id(3), pin_id(4)]),
799 PinDir::Output,
800 &mut builder,
801 );
802 seals_to_nothing(builder);
803 }
804
805 #[test]
810 fn a_locked_owner_still_takes_presentation_edits() {
811 let doc = locked_scene();
812 let mut builder = CommitBuilder::new("Hid a frozen block's tags");
813 set_tag_visibility(
814 &doc,
815 &[pin_id(3), pin_id(4)],
816 TagVisibility::Hidden,
817 &mut builder,
818 );
819 assert!(
820 builder.seal().is_some(),
821 "tag visibility is presentation, and a lock does not reach it",
822 );
823 }
824
825 #[test]
826 fn cycle_dir_visits_every_direction_and_returns() {
827 let mut doc = wired();
828 assert_eq!(
829 pin(&doc, 3).dir,
830 PinDir::Input,
831 "precondition: the fixture pin starts at the zero direction"
832 );
833
834 for expected in [PinDir::Output, PinDir::InOut, PinDir::Input] {
835 let mut builder = CommitBuilder::new("Cycled a pin");
836 cycle_dir(&doc, material(&doc, 3), &mut builder);
837 doc = fold(builder, &doc);
838 assert_eq!(pin(&doc, 3).dir, expected);
839 }
840 }
841
842 #[test]
845 fn an_absent_pin_has_no_capability_to_cycle() {
846 let doc = wired();
847 assert_eq!(MaterialPin::of(&doc, pin_id(99)), None);
848 }
849
850 #[test]
851 fn set_dirs_writes_the_group_and_skips_the_pins_already_facing_that_way() {
852 let doc = wired();
853 assert_eq!(
854 pin(&doc, 4).dir,
855 PinDir::Input,
856 "precondition: both scene pins start at the zero direction"
857 );
858
859 let mut builder = CommitBuilder::new("Redirected a group");
860 set_dirs(
861 &doc,
862 &MaterialPin::all(&doc, &[pin_id(3), pin_id(4)]),
863 PinDir::Output,
864 &mut builder,
865 );
866 let doc = fold(builder, &doc);
867 assert_eq!(pin(&doc, 3).dir, PinDir::Output);
868 assert_eq!(pin(&doc, 4).dir, PinDir::Output);
869
870 let mut builder = CommitBuilder::new("Redirected them again");
871 set_dirs(
872 &doc,
873 &MaterialPin::all(&doc, &[pin_id(3), pin_id(4)]),
874 PinDir::Output,
875 &mut builder,
876 );
877 seals_to_nothing(builder);
878 }
879
880 #[test]
881 fn set_tag_visibility_hides_a_group_and_a_locked_owner_does_not_stop_it() {
882 let doc = locked_scene();
883 assert!(
884 !pin(&doc, 3).tag_hidden,
885 "precondition: the fixture pins draw their tags"
886 );
887
888 let mut builder = CommitBuilder::new("Hid the tags");
889 set_tag_visibility(
890 &doc,
891 &[pin_id(3), pin_id(4)],
892 TagVisibility::Hidden,
893 &mut builder,
894 );
895 let doc = fold(builder, &doc);
896 assert!(pin(&doc, 3).tag_hidden);
897 assert!(pin(&doc, 4).tag_hidden);
898
899 let mut builder = CommitBuilder::new("Hid them again");
900 set_tag_visibility(
901 &doc,
902 &[pin_id(3), pin_id(4)],
903 TagVisibility::Hidden,
904 &mut builder,
905 );
906 seals_to_nothing(builder);
907
908 let mut builder = CommitBuilder::new("Showed the tags");
909 set_tag_visibility(&doc, &[pin_id(3)], TagVisibility::Shown, &mut builder);
910 let doc = fold(builder, &doc);
911 assert!(!pin(&doc, 3).tag_hidden);
912 assert!(
913 pin(&doc, 4).tag_hidden,
914 "the pin outside the group is untouched"
915 );
916 }
917
918 #[test]
919 fn set_locked_flips_the_flag_once() {
920 let doc = locked_scene();
921
922 let mut builder = CommitBuilder::new("Locked it again");
923 set_locked(&doc, block_id(1), InterfaceLock::Locked, &mut builder);
924 seals_to_nothing(builder);
925
926 let mut builder = CommitBuilder::new("Unlocked the block");
927 set_locked(&doc, block_id(1), InterfaceLock::Unlocked, &mut builder);
928 let doc = fold(builder, &doc);
929 assert!(
930 !doc.block(&block_id(1))
931 .expect("the scene's block exists")
932 .locked
933 );
934
935 let mut builder = CommitBuilder::new("Renamed a thawed pin");
936 rename_pin(
937 &doc,
938 material(&doc, 3),
939 "clk",
940 LabelFitWidth::new(5),
941 &mut builder,
942 );
943 let doc = fold(builder, &doc);
944 assert_eq!(
945 pin(&doc, 3).name,
946 "clk",
947 "thawing restores the pin interface"
948 );
949 }
950
951 #[test]
952 fn rename_route_writes_only_a_non_empty_name() {
953 let doc = wired();
954 assert_eq!(
955 doc.route(&route_id(5))
956 .expect("the scene's route exists")
957 .name,
958 "r5",
959 "precondition: the scene's wire starts under its fixture name"
960 );
961
962 let mut builder = CommitBuilder::new("Named a wire");
963 rename_route(&doc, route_id(5), "clk_net", &mut builder);
964 let doc = fold(builder, &doc);
965 assert_eq!(
966 doc.route(&route_id(5))
967 .expect("the scene's route exists")
968 .name,
969 "clk_net"
970 );
971
972 for blank in ["", " "] {
973 let mut builder = CommitBuilder::new("Blanked a wire name");
974 rename_route(&doc, route_id(5), blank, &mut builder);
975 seals_to_nothing(builder);
976 }
977
978 let mut builder = CommitBuilder::new("Named it the same");
979 rename_route(&doc, route_id(5), "clk_net", &mut builder);
980 seals_to_nothing(builder);
981 }
982
983 #[test]
984 fn place_label_repositions_all_three_labels() {
985 let doc = wired();
986 let placement = LabelPlacement {
987 offset: FracVal::from(2.5),
988 side: Some(LabelSide::Bottom),
989 };
990 assert_eq!(
991 doc.block(&block_id(1))
992 .expect("the scene's block exists")
993 .title
994 .side,
995 LabelSide::Top,
996 "precondition: labels start at the zero placement"
997 );
998
999 let mut builder = CommitBuilder::new("Placed the labels");
1000 place_label(
1001 &doc,
1002 LabelTarget::BlockTitle(block_id(1)),
1003 placement,
1004 &mut builder,
1005 );
1006 place_label(
1007 &doc,
1008 LabelTarget::BlockType(block_id(1)),
1009 placement,
1010 &mut builder,
1011 );
1012 place_label(
1013 &doc,
1014 LabelTarget::AreaTitle(area_id(8)),
1015 placement,
1016 &mut builder,
1017 );
1018 let doc = fold(builder, &doc);
1019
1020 let block = doc.block(&block_id(1)).expect("the scene's block exists");
1021 for label in [&block.title, &block.type_label] {
1022 assert_eq!(label.offset, FracVal::from(2.5));
1023 assert_eq!(label.side, LabelSide::Bottom);
1024 }
1025 let area_title = &doc
1026 .area(&area_id(8))
1027 .expect("the scene's area exists")
1028 .title;
1029 assert_eq!(area_title.offset, FracVal::from(2.5));
1030 assert_eq!(area_title.side, LabelSide::Bottom);
1031
1032 let mut builder = CommitBuilder::new("Placed it where it already was");
1033 place_label(
1034 &doc,
1035 LabelTarget::BlockTitle(block_id(1)),
1036 placement,
1037 &mut builder,
1038 );
1039 seals_to_nothing(builder);
1040 }
1041
1042 #[test]
1043 fn a_placement_writes_only_the_register_that_moved() {
1044 let doc = wired();
1045
1046 let mut builder = CommitBuilder::new("Slid a title along its side");
1047 place_label(
1048 &doc,
1049 LabelTarget::BlockTitle(block_id(1)),
1050 LabelPlacement {
1051 offset: FracVal::from(1.0),
1052 side: None,
1053 },
1054 &mut builder,
1055 );
1056 let doc = fold(builder, &doc);
1057 let title = &doc
1058 .block(&block_id(1))
1059 .expect("the scene's block exists")
1060 .title;
1061 assert_eq!(title.offset, FracVal::from(1.0));
1062 assert_eq!(
1063 title.side,
1064 LabelSide::Top,
1065 "a drag that chose no side leaves the side alone"
1066 );
1067
1068 let mut builder = CommitBuilder::new("Dropped it on another side");
1069 place_label(
1070 &doc,
1071 LabelTarget::BlockTitle(block_id(1)),
1072 LabelPlacement {
1073 offset: FracVal::from(1.0),
1074 side: Some(LabelSide::Center),
1075 },
1076 &mut builder,
1077 );
1078 let commit = builder.seal().expect("the side moved");
1079 assert_eq!(
1080 commit.ops().len(),
1081 1,
1082 "an unmoved offset takes no write of its own"
1083 );
1084 let doc = doc
1085 .try_apply(&commit)
1086 .expect("the fold accepts the gesture");
1087 assert_eq!(
1088 doc.block(&block_id(1))
1089 .expect("the scene's block exists")
1090 .title
1091 .side,
1092 LabelSide::Center
1093 );
1094 }
1095
1096 #[test]
1097 fn edit_text_writes_content_and_discards_an_emptied_box() {
1098 let doc = wired();
1099 assert!(
1100 doc.text(&text_id(7)).is_some(),
1101 "precondition: the scene holds the text box"
1102 );
1103
1104 let mut builder = CommitBuilder::new("Edited a text box");
1105 edit_text(&doc, text_id(7), "two\nlines", &mut builder);
1106 let doc = fold(builder, &doc);
1107 assert_eq!(
1108 doc.text(&text_id(7)).expect("the scene's text exists").text,
1109 "two\nlines"
1110 );
1111
1112 let mut builder = CommitBuilder::new("Edited it to the same content");
1113 edit_text(&doc, text_id(7), "two\nlines", &mut builder);
1114 seals_to_nothing(builder);
1115
1116 let mut builder = CommitBuilder::new("Emptied a text box");
1117 edit_text(&doc, text_id(7), " \n ", &mut builder);
1118 let doc = fold(builder, &doc);
1119 assert!(
1120 doc.text(&text_id(7)).is_none(),
1121 "an emptied box is discarded, not left invisible"
1122 );
1123
1124 let mut builder = CommitBuilder::new("Emptied it again");
1125 edit_text(&doc, text_id(7), "", &mut builder);
1126 seals_to_nothing(builder);
1127 }
1128
1129 #[test]
1130 fn clearing_a_wire_label_deletes_it_and_unnames_the_wire() {
1131 let doc = wired();
1132 let mut builder = CommitBuilder::new("Labelled a wire");
1133 builder.push(route_label_create(6, 5));
1134 builder.push(route_label_create(9, 5));
1135 let doc = fold(builder, &doc);
1136 assert_eq!(
1137 doc.route_label(&route_label_id(6))
1138 .expect("the label exists")
1139 .owner,
1140 route_id(5),
1141 "precondition: the label belongs to the scene's named wire"
1142 );
1143
1144 let mut builder = CommitBuilder::new("Cleared a wire label");
1145 clear_wire_label(&doc, route_label_id(6), &mut builder);
1146 let doc = fold(builder, &doc);
1147 assert!(doc.route_label(&route_label_id(6)).is_none());
1148 assert_eq!(
1149 doc.route(&route_id(5))
1150 .expect("the scene's route exists")
1151 .name,
1152 "",
1153 "the wire goes back to unlabelled rather than carrying a blank slot"
1154 );
1155
1156 let mut builder = CommitBuilder::new("Cleared the second label");
1157 clear_wire_label(&doc, route_label_id(9), &mut builder);
1158 let commit = builder.seal().expect("the label is deleted");
1159 assert_eq!(
1160 commit.ops().len(),
1161 1,
1162 "an already-unnamed wire takes no name write"
1163 );
1164
1165 let doc = doc
1166 .try_apply(&commit)
1167 .expect("the fold accepts the gesture");
1168 let mut builder = CommitBuilder::new("Cleared a ghost label");
1169 clear_wire_label(&doc, route_label_id(9), &mut builder);
1170 seals_to_nothing(builder);
1171 }
1172}