max / quasi
5 files changed,
+264 insertions,
-11 deletions
| @@ -2348,9 +2348,9 @@ | |||
| 2348 | 2348 | ||
| 2349 | 2349 | [[package]] | |
| 2350 | 2350 | name = "makeover-immediate" | |
| 2351 | - | version = "0.25.0" | |
| 2351 | + | version = "0.26.0" | |
| 2352 | 2352 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2353 | - | checksum = "c78ddbf237c617715e8ed0c174a46f52d7135a388a0f786a164acc2bca8cb97f" | |
| 2353 | + | checksum = "e9946bbd05379c4aa025389d129c44440006c4aad2ec039a9e92ef7061b95191" | |
| 2354 | 2354 | dependencies = [ | |
| 2355 | 2355 | "egui", | |
| 2356 | 2356 | "egui_extras", | |
| @@ -5624,14 +5624,6 @@ | |||
| 5624 | 5624 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5625 | 5625 | checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" | |
| 5626 | 5626 | ||
| 5627 | - | [[patch.unused]] | |
| 5628 | - | name = "synckit-client" | |
| 5629 | - | version = "0.8.0" | |
| 5630 | - | ||
| 5631 | - | [[patch.unused]] | |
| 5632 | - | name = "synckit-config" | |
| 5633 | - | version = "0.2.0" | |
| 5634 | - | ||
| 5635 | 5627 | [[patch.unused]] | |
| 5636 | 5628 | name = "kberg" | |
| 5637 | 5629 | version = "0.1.0" | |
| @@ -5647,3 +5639,11 @@ | |||
| 5647 | 5639 | [[patch.unused]] | |
| 5648 | 5640 | name = "tagtree" | |
| 5649 | 5641 | version = "0.4.0" | |
| 5642 | + | ||
| 5643 | + | [[patch.unused]] | |
| 5644 | + | name = "synckit-client" | |
| 5645 | + | version = "0.8.0" | |
| 5646 | + | ||
| 5647 | + | [[patch.unused]] | |
| 5648 | + | name = "synckit-config" | |
| 5649 | + | version = "0.2.0" |
| @@ -17,7 +17,7 @@ | |||
| 17 | 17 | # widget here that is not a container comes from it: a screen walk that painted | |
| 18 | 18 | # its own meter would be the divergence the suite exists to end, one copy per | |
| 19 | 19 | # renderer instead of one copy per app. | |
| 20 | - | makeover-immediate = "0.25.0" | |
| 20 | + | makeover-immediate = "0.26.0" | |
| 21 | 21 | egui = { version = "0.35", default-features = false } | |
| 22 | 22 | # Markdown to text for `Node::Rich`. The same crate the webview renderer takes, | |
| 23 | 23 | # reading the same source: what differs is that it can emit markup and this |
| @@ -25,6 +25,7 @@ | |||
| 25 | 25 | bevel_dark: Color32::BLACK, | |
| 26 | 26 | elevation: Color32::from_black_alpha(46), | |
| 27 | 27 | content: Color32::from_rgb(6, 6, 6), | |
| 28 | + | content_secondary: Color32::from_rgb(66, 66, 66), | |
| 28 | 29 | content_muted: Color32::from_rgb(7, 7, 7), | |
| 29 | 30 | action: Color32::from_rgb(8, 8, 8), | |
| 30 | 31 | danger: Color32::from_rgb(9, 9, 9), | |
| @@ -796,3 +797,105 @@ | |||
| 796 | 797 | }]); | |
| 797 | 798 | draw(&screen, &mut view); | |
| 798 | 799 | } | |
| 800 | + | ||
| 801 | + | /// One of every shape that could plausibly cache a size. | |
| 802 | + | /// | |
| 803 | + | /// A field at each [`layout::Width`], a table with mixed | |
| 804 | + | /// [`layout::Priority`], a list that says how much more there is, and a nested | |
| 805 | + | /// region. Written out here rather than shared with the other two renderers' | |
| 806 | + | /// copies: the fixture is a few lines, and sharing it would mean a new public | |
| 807 | + | /// surface on a crate for the sake of a test. | |
| 808 | + | fn every_shape_that_could_cache() -> Screen { | |
| 809 | + | Screen::sidebar_content("Any width").with( | |
| 810 | + | Slot::new("main", RegionKind::Pane) | |
| 811 | + | .with(Node::Field(Box::new(Field::new( | |
| 812 | + | layout::FieldKind::Text, | |
| 813 | + | "wide", | |
| 814 | + | "Wide", | |
| 815 | + | )))) | |
| 816 | + | .with(Node::Field(Box::new( | |
| 817 | + | Field::new(layout::FieldKind::Text, "tight", "Tight").width(layout::Width::Content), | |
| 818 | + | ))) | |
| 819 | + | .with(Node::Field(Box::new( | |
| 820 | + | Field::new(layout::FieldKind::Text, "held", "Held").width(layout::Width::Fixed), | |
| 821 | + | ))) | |
| 822 | + | .with(Node::Table { | |
| 823 | + | columns: vec![ | |
| 824 | + | quasi_router::Column::new("Name").priority(layout::Priority::Essential), | |
| 825 | + | quasi_router::Column::new("Kind").priority(layout::Priority::Secondary), | |
| 826 | + | quasi_router::Column::new("Added").priority(layout::Priority::Optional), | |
| 827 | + | ], | |
| 828 | + | rows: vec![quasi_router::Cells::new([ | |
| 829 | + | "kick.wav", | |
| 830 | + | "sample", | |
| 831 | + | "2026-08-12", | |
| 832 | + | ])], | |
| 833 | + | more: Some(quasi_router::Rest::more(1, Action::get("/samples?from=1"))), | |
| 834 | + | }) | |
| 835 | + | .with(Node::List { | |
| 836 | + | rows: vec![quasi_router::Row::new("one"), quasi_router::Row::new("two")], | |
| 837 | + | more: Some(quasi_router::Rest::more(2, Action::get("/rows?from=2"))), | |
| 838 | + | }) | |
| 839 | + | .with(Node::Region( | |
| 840 | + | Slot::group("nested").with(Node::text("inside")), | |
| 841 | + | )), | |
| 842 | + | ) | |
| 843 | + | } | |
| 844 | + | ||
| 845 | + | /// Everything the renderer put on the screen at this width, as text. | |
| 846 | + | /// | |
| 847 | + | /// Through a real `Context` rather than `__run_test_ui`, because the width is | |
| 848 | + | /// the whole subject here and the test helper picks its own. The shapes are | |
| 849 | + | /// compared by their debug form: what is being asserted is that two frames are | |
| 850 | + | /// the same picture, and equality of the primitives is the strongest available | |
| 851 | + | /// statement of that. | |
| 852 | + | fn painted( | |
| 853 | + | ctx: &egui::Context, | |
| 854 | + | immediate: &Immediate, | |
| 855 | + | screen: &Screen, | |
| 856 | + | view: &mut View, | |
| 857 | + | width: f32, | |
| 858 | + | ) -> String { | |
| 859 | + | let input = egui::RawInput { | |
| 860 | + | screen_rect: Some(egui::Rect::from_min_size( | |
| 861 | + | egui::Pos2::ZERO, | |
| 862 | + | egui::vec2(width, 900.0), | |
| 863 | + | )), | |
| 864 | + | ..Default::default() | |
| 865 | + | }; | |
| 866 | + | let output = ctx.run_ui(input, |ctx| { | |
| 867 | + | egui::CentralPanel::default().show(ctx, |ui| { | |
| 868 | + | immediate.screen(ui, screen, view); | |
| 869 | + | }); | |
| 870 | + | }); | |
| 871 | + | format!("{:?}", output.shapes) | |
| 872 | + | } | |
| 873 | + | ||
| 874 | + | #[test] | |
| 875 | + | fn a_frame_is_the_same_picture_however_the_window_got_here() { | |
| 876 | + | // "Any width, one answer", `makeover-layout` 0.27.4. The same description | |
| 877 | + | // at the same width is the same frame, whatever widths came before it. | |
| 878 | + | // | |
| 879 | + | // The renderer, the context and the view are made once and reused across | |
| 880 | + | // the sequence, which is the half that matters: a fresh `Immediate` per | |
| 881 | + | // frame could not fail this test however much geometry it kept. egui makes | |
| 882 | + | // the property easy to lose rather than easy to keep -- an immediate-mode | |
| 883 | + | // library hands you `ui.available_width()` every frame and a memory store | |
| 884 | + | // to put the answer in -- so this is the renderer where the guard earns | |
| 885 | + | // its place. | |
| 886 | + | let screen = every_shape_that_could_cache(); | |
| 887 | + | let ctx = egui::Context::default(); | |
| 888 | + | let immediate = renderer(); | |
| 889 | + | let mut view = View::new(); | |
| 890 | + | ||
| 891 | + | let cold = painted(&ctx, &immediate, &screen, &mut view, 400.0); | |
| 892 | + | ||
| 893 | + | for width in [1200.0, 320.0, 900.0, 200.0] { | |
| 894 | + | let _ = painted(&ctx, &immediate, &screen, &mut view, width); | |
| 895 | + | } | |
| 896 | + | assert_eq!(painted(&ctx, &immediate, &screen, &mut view, 400.0), cold); | |
| 897 | + | ||
| 898 | + | // And the fixture is one the width actually moves, or the assertion above | |
| 899 | + | // would be true of a blank frame. | |
| 900 | + | assert_ne!(painted(&ctx, &immediate, &screen, &mut view, 1200.0), cold); | |
| 901 | + | } |
| @@ -1667,3 +1667,77 @@ | |||
| 1667 | 1667 | assert!(out.contains("first") && out.contains("second"), "{out}"); | |
| 1668 | 1668 | assert!(!out.contains("< Prev >"), "{out}"); | |
| 1669 | 1669 | } | |
| 1670 | + | ||
| 1671 | + | /// One of every shape that could plausibly cache a size. | |
| 1672 | + | /// | |
| 1673 | + | /// A field at each [`layout::Width`], a table with mixed | |
| 1674 | + | /// [`layout::Priority`], a list that says how much more there is, and a nested | |
| 1675 | + | /// region. Written out here rather than shared with the other two renderers' | |
| 1676 | + | /// copies of it: the fixture is a few lines and sharing it would mean a new | |
| 1677 | + | /// public surface on a crate for the sake of a test. | |
| 1678 | + | fn every_shape_that_could_cache() -> Screen { | |
| 1679 | + | Screen::sidebar_content("Any width").with( | |
| 1680 | + | Slot::new("main", RegionKind::Pane) | |
| 1681 | + | .with(Node::Field(Box::new(Field::new( | |
| 1682 | + | layout::FieldKind::Text, | |
| 1683 | + | "wide", | |
| 1684 | + | "Wide", | |
| 1685 | + | )))) | |
| 1686 | + | .with(Node::Field(Box::new( | |
| 1687 | + | Field::new(layout::FieldKind::Text, "tight", "Tight").width(layout::Width::Content), | |
| 1688 | + | ))) | |
| 1689 | + | .with(Node::Field(Box::new( | |
| 1690 | + | Field::new(layout::FieldKind::Text, "held", "Held").width(layout::Width::Fixed), | |
| 1691 | + | ))) | |
| 1692 | + | .with(Node::Table { | |
| 1693 | + | columns: vec![ | |
| 1694 | + | Column::new("Name").priority(layout::Priority::Essential), | |
| 1695 | + | Column::new("Kind").priority(layout::Priority::Secondary), | |
| 1696 | + | Column::new("Added").priority(layout::Priority::Optional), | |
| 1697 | + | ], | |
| 1698 | + | rows: vec![Cells::new(["kick.wav", "sample", "2026-08-12"])], | |
| 1699 | + | more: Some(Rest::more(1, Action::get("/samples?from=1"))), | |
| 1700 | + | }) | |
| 1701 | + | .with(Node::List { | |
| 1702 | + | rows: vec![Row::new("one"), Row::new("two")], | |
| 1703 | + | more: Some(Rest::more(2, Action::get("/rows?from=2"))), | |
| 1704 | + | }) | |
| 1705 | + | .with(Node::Region( | |
| 1706 | + | Slot::group("nested").with(Node::text("inside")), | |
| 1707 | + | )), | |
| 1708 | + | ) | |
| 1709 | + | } | |
| 1710 | + | ||
| 1711 | + | #[test] | |
| 1712 | + | fn a_narrow_terminal_draws_the_same_thing_however_it_got_narrow() { | |
| 1713 | + | // "Any width, one answer", `makeover-layout` 0.27.4. The same description | |
| 1714 | + | // at the same width is the same picture, whatever widths came before it. | |
| 1715 | + | // | |
| 1716 | + | // The renderer and the view are made once and reused across the sequence, | |
| 1717 | + | // which is the half that matters: a fresh `Tui` per draw could not fail | |
| 1718 | + | // this test however much geometry it kept. What it catches is a renderer | |
| 1719 | + | // that remembers -- a cutoff cached on the first pass, a column measure | |
| 1720 | + | // stored beside the theme -- and that is exactly the shape of the bug that | |
| 1721 | + | // makes an ordinary page's sidebar depend on the order you dragged the | |
| 1722 | + | // window. | |
| 1723 | + | let screen = every_shape_that_could_cache(); | |
| 1724 | + | let tui = tui(); | |
| 1725 | + | let view = View::new(); | |
| 1726 | + | ||
| 1727 | + | let draw = |width: u16| { | |
| 1728 | + | let area = Rect::new(0, 0, width, 24); | |
| 1729 | + | let mut buf = Buffer::empty(area); | |
| 1730 | + | tui.screen(&screen, &view, area, &mut buf); | |
| 1731 | + | rows(&buf) | |
| 1732 | + | }; | |
| 1733 | + | ||
| 1734 | + | let cold = draw(40); | |
| 1735 | + | for width in [120, 200, 40, 12, 400] { | |
| 1736 | + | let _ = draw(width); | |
| 1737 | + | } | |
| 1738 | + | assert_eq!(draw(40), cold); | |
| 1739 | + | ||
| 1740 | + | // And the fixture is one that narrowing actually bites, or the assertion | |
| 1741 | + | // above would be true of an empty screen. | |
| 1742 | + | assert_ne!(draw(120), cold); | |
| 1743 | + | } |
| @@ -3304,3 +3304,79 @@ | |||
| 3304 | 3304 | assert_eq!(both.matches("field-writes").count(), 1); | |
| 3305 | 3305 | assert!(both.contains("data-width=\"content\"")); | |
| 3306 | 3306 | } | |
| 3307 | + | ||
| 3308 | + | /// One of every shape that could plausibly cache a size. | |
| 3309 | + | /// | |
| 3310 | + | /// A field at each [`layout::Width`], a table with mixed | |
| 3311 | + | /// [`layout::Priority`], a list that says how much more there is, and a nested | |
| 3312 | + | /// region. Shared by the three renderers' path-independence tests, written out | |
| 3313 | + | /// in each rather than lifted into a crate they would all have to depend on: | |
| 3314 | + | /// the fixture is six lines and a shared one would be a new public surface. | |
| 3315 | + | fn every_shape_that_could_cache() -> Screen { | |
| 3316 | + | Screen::sidebar_content("Any width").with( | |
| 3317 | + | Slot::new("main", RegionKind::Pane) | |
| 3318 | + | .with(Node::Field(Box::new(Field::new( | |
| 3319 | + | layout::FieldKind::Text, | |
| 3320 | + | "wide", | |
| 3321 | + | "Wide", | |
| 3322 | + | )))) | |
| 3323 | + | .with(Node::Field(Box::new( | |
| 3324 | + | Field::new(layout::FieldKind::Text, "tight", "Tight").width(layout::Width::Content), | |
| 3325 | + | ))) | |
| 3326 | + | .with(Node::Field(Box::new( | |
| 3327 | + | Field::new(layout::FieldKind::Text, "held", "Held").width(layout::Width::Fixed), | |
| 3328 | + | ))) | |
| 3329 | + | .with(Node::Table { | |
| 3330 | + | columns: vec![ | |
| 3331 | + | Column::new("Name").priority(layout::Priority::Essential), | |
| 3332 | + | Column::new("Kind").priority(layout::Priority::Secondary), | |
| 3333 | + | Column::new("Added").priority(layout::Priority::Optional), | |
| 3334 | + | ], | |
| 3335 | + | rows: vec![Cells::new(["kick.wav", "sample", "2026-08-12"])], | |
| 3336 | + | more: Some(Rest::more(1, Action::get("/samples?from=1"))), | |
| 3337 | + | }) | |
| 3338 | + | .with(Node::List { | |
| 3339 | + | rows: vec![Row::new("one"), Row::new("two")], | |
| 3340 | + | more: Some(Rest::more(2, Action::get("/rows?from=2"))), | |
| 3341 | + | }) | |
| 3342 | + | .with(Node::Region( | |
| 3343 | + | Slot::group("nested").with(Node::text("inside")), | |
| 3344 | + | )), | |
| 3345 | + | ) | |
| 3346 | + | } | |
| 3347 | + | ||
| 3348 | + | #[test] | |
| 3349 | + | fn the_markup_is_not_a_function_of_the_width() { | |
| 3350 | + | // "Any width, one answer", `makeover-layout` 0.27.4. This renderer keeps | |
| 3351 | + | // the property the cheapest way there is: it is never told the width, so | |
| 3352 | + | // narrowing is the stylesheet's and lives in `@media`, where there is | |
| 3353 | + | // nowhere to keep the width you came from. | |
| 3354 | + | // | |
| 3355 | + | // Two halves. The first is that a reused renderer answers the same as a | |
| 3356 | + | // fresh one, which is what would fail the day someone memoised a track | |
| 3357 | + | // list on the `Webview`. The second is that the document carries no | |
| 3358 | + | // measurement at all, which is what would fail the day someone reached for | |
| 3359 | + | // an inline `style` to size something -- an inline width is a number | |
| 3360 | + | // computed once, and a number computed once is the previous frame's | |
| 3361 | + | // answer. | |
| 3362 | + | let screen = every_shape_that_could_cache(); | |
| 3363 | + | ||
| 3364 | + | let reused = Webview::new(); | |
| 3365 | + | let first = reused.screen(&screen); | |
| 3366 | + | for _ in 0..4 { | |
| 3367 | + | assert_eq!(reused.screen(&screen), first); | |
| 3368 | + | } | |
| 3369 | + | assert_eq!(Webview::new().screen(&screen), first); | |
| 3370 | + | ||
| 3371 | + | // No absolute length reaches the markup. The one inline style the | |
| 3372 | + | // document carries is the arrangement's share, and it is in `fr`: a | |
| 3373 | + | // proportion of whatever there turns out to be, which is a fact the | |
| 3374 | + | // description stated rather than a size this renderer worked out. | |
| 3375 | + | for style in first.split("style=\"").skip(1) { | |
| 3376 | + | let value = style.split('"').next().expect("an attribute closes"); | |
| 3377 | + | for unit in ["px", "vw", "vh", "ch", "pt"] { | |
| 3378 | + | assert!(!value.contains(unit), "{unit} in {value}"); | |
| 3379 | + | } | |
| 3380 | + | assert!(value.contains("fr"), "{value}"); | |
| 3381 | + | } | |
| 3382 | + | } |