max / quasi
6 files changed,
+144 insertions,
-8 deletions
| @@ -3563,7 +3563,7 @@ | |||
| 3563 | 3563 | ||
| 3564 | 3564 | [[package]] | |
| 3565 | 3565 | name = "quasi-router" | |
| 3566 | - | version = "0.101.11" | |
| 3566 | + | version = "0.101.12" | |
| 3567 | 3567 | dependencies = [ | |
| 3568 | 3568 | "makeover-layout", | |
| 3569 | 3569 | ] | |
| @@ -6279,6 +6279,10 @@ | |||
| 6279 | 6279 | "winnow 1.0.4", | |
| 6280 | 6280 | ] | |
| 6281 | 6281 | ||
| 6282 | + | [[patch.unused]] | |
| 6283 | + | name = "quasi-type" | |
| 6284 | + | version = "0.1.3" | |
| 6285 | + | ||
| 6282 | 6286 | [[patch.unused]] | |
| 6283 | 6287 | name = "synckit-client" | |
| 6284 | 6288 | version = "0.10.0" | |
| @@ -6298,7 +6302,3 @@ | |||
| 6298 | 6302 | [[patch.unused]] | |
| 6299 | 6303 | name = "tagtree" | |
| 6300 | 6304 | version = "0.4.1" | |
| 6301 | - | ||
| 6302 | - | [[patch.unused]] | |
| 6303 | - | name = "quasi-type" | |
| 6304 | - | version = "0.1.3" |
| @@ -1,6 +1,6 @@ | |||
| 1 | 1 | [package] | |
| 2 | 2 | name = "quasi-router" | |
| 3 | - | version = "0.101.11" | |
| 3 | + | version = "0.101.12" | |
| 4 | 4 | description = "Host-agnostic router: a request in, a renderer-agnostic description out" | |
| 5 | 5 | edition.workspace = true | |
| 6 | 6 | rust-version.workspace = true |
| @@ -264,6 +264,15 @@ | |||
| 264 | 264 | priority: Ident, | |
| 265 | 265 | inner: Box<Emission>, | |
| 266 | 266 | }, | |
| 267 | + | /// `at <arg> <emission>` -- one thing on an axis, and where it sits. | |
| 268 | + | /// | |
| 269 | + | /// Only inside a `timeline`, where the argument is a `Placement` and the | |
| 270 | + | /// emission is the row that sits at it. The keyword is `cell at <arg>`'s, | |
| 271 | + | /// and the container is the whole of how the two are told apart, exactly as | |
| 272 | + | /// it is for `beside` and for `include`. A timeline holds nothing but | |
| 273 | + | /// entries and a row of cells holds nothing but cells, so no body can be | |
| 274 | + | /// read both ways. | |
| 275 | + | At { at: Arg, inner: Box<Emission> }, | |
| 267 | 276 | /// `<emission> when <predicate>` / `unless <predicate>`. | |
| 268 | 277 | /// | |
| 269 | 278 | /// A guard suppresses the emission and nothing else. R9: the holes inside |
| @@ -52,6 +52,8 @@ | |||
| 52 | 52 | Canvas, | |
| 53 | 53 | /// One tag: its body says what it is like, and it emits nothing. | |
| 54 | 54 | Tag, | |
| 55 | + | /// An axis: its members are `at`, each a placement and the row on it. | |
| 56 | + | Timeline, | |
| 55 | 57 | } | |
| 56 | 58 | ||
| 57 | 59 | pub fn declaration(declaration: &Declaration) -> Result<TokenStream> { | |
| @@ -686,6 +688,51 @@ | |||
| 686 | 688 | emission_span(other), | |
| 687 | 689 | "a column holds no members: its body says how it narrows", | |
| 688 | 690 | )), | |
| 691 | + | (Container::Timeline, Emission::At { at, inner }) => { | |
| 692 | + | let at = arg(at)?; | |
| 693 | + | let row = match &**inner { | |
| 694 | + | Emission::Row { primary, body } => self::row(primary, body)?, | |
| 695 | + | // A supplier, the same courtesy `include` is everywhere else: | |
| 696 | + | // goingson's day view draws the axis and the all-day strip from | |
| 697 | + | // one row shape, so the row is a shape away from both. | |
| 698 | + | Emission::Include(supplier) => { | |
| 699 | + | let supplier = hole(supplier)?; | |
| 700 | + | quote!(::std::convert::Into::into(#supplier)) | |
| 701 | + | } | |
| 702 | + | other => { | |
| 703 | + | return Err(syn::Error::new( | |
| 704 | + | emission_span(other), | |
| 705 | + | "an axis holds rows: write `at <placement> row <primary> { .. }`", | |
| 706 | + | )); | |
| 707 | + | } | |
| 708 | + | }; | |
| 709 | + | Ok(( | |
| 710 | + | quote!(::quasi_router::screen::Placed { | |
| 711 | + | placement: #at, | |
| 712 | + | row: #row, | |
| 713 | + | }), | |
| 714 | + | quote!(.placed(#held)), | |
| 715 | + | )) | |
| 716 | + | } | |
| 717 | + | (Container::Timeline, other) => Err(syn::Error::new( | |
| 718 | + | emission_span(other), | |
| 719 | + | "an axis holds what sits on it: write `at <placement> row <primary> { .. }`", | |
| 720 | + | )), | |
| 721 | + | ( | |
| 722 | + | Container::Act, | |
| 723 | + | Emission::Field { | |
| 724 | + | kind, | |
| 725 | + | name, | |
| 726 | + | label, | |
| 727 | + | body, | |
| 728 | + | }, | |
| 729 | + | ) => { | |
| 730 | + | // `Act::asking`: a control that wants a value before it acts. | |
| 731 | + | // goingson's day view is the site -- placing a task asks which slot | |
| 732 | + | // and, when the task carries no estimate, how long it takes. | |
| 733 | + | let asked = field(kind, name, label, body)?; | |
| 734 | + | Ok((asked, quote!(.asking(#held)))) | |
| 735 | + | } | |
| 689 | 736 | (Container::Act | Container::Field, other) => Err(syn::Error::new( | |
| 690 | 737 | emission_span(other), | |
| 691 | 738 | "this holds no members: its body says what it is like", | |
| @@ -840,6 +887,18 @@ | |||
| 840 | 887 | )?; | |
| 841 | 888 | Ok(quote!(::quasi_router::Node::Canvas(::std::boxed::Box::new(#canvas)))) | |
| 842 | 889 | } | |
| 890 | + | Emission::Simple { member, args, body } if member == "timeline" => { | |
| 891 | + | // The fifth member whose body is not `Node`'s own, and the only one | |
| 892 | + | // that holds members rather than settings: an axis is told what | |
| 893 | + | // sits on it and where. `Node::timeline` answers with the variant | |
| 894 | + | // already, so nothing is wrapped afterwards. | |
| 895 | + | let args = args.iter().map(self::arg).collect::<Result<Vec<_>>>()?; | |
| 896 | + | accrete( | |
| 897 | + | body, | |
| 898 | + | Container::Timeline, | |
| 899 | + | "e!(::quasi_router::Node::timeline(#(#args),*)), | |
| 900 | + | ) | |
| 901 | + | } | |
| 843 | 902 | Emission::Simple { member, args, body } => { | |
| 844 | 903 | let args = args.iter().map(self::arg).collect::<Result<Vec<_>>>()?; | |
| 845 | 904 | accrete( | |
| @@ -938,6 +997,10 @@ | |||
| 938 | 997 | priority.span(), | |
| 939 | 998 | "`beside` needs a run in scope, which is rule R3", | |
| 940 | 999 | )), | |
| 1000 | + | Emission::At { inner, .. } => Err(syn::Error::new( | |
| 1001 | + | emission_span(inner), | |
| 1002 | + | "`at` places a row on an axis; it needs a `timeline` in scope", | |
| 1003 | + | )), | |
| 941 | 1004 | } | |
| 942 | 1005 | } | |
| 943 | 1006 | ||
| @@ -1492,6 +1555,7 @@ | |||
| 1492 | 1555 | }, | |
| 1493 | 1556 | Emission::Across { fallback, .. } => fallback.span(), | |
| 1494 | 1557 | Emission::Beside { priority, .. } => priority.span(), | |
| 1558 | + | Emission::At { inner, .. } => emission_span(inner), | |
| 1495 | 1559 | Emission::Table(_) | Emission::Cells(_) => Span::call_site(), | |
| 1496 | 1560 | Emission::Column { .. } | Emission::Cell { .. } => Span::call_site(), | |
| 1497 | 1561 | Emission::Activate(action) => action.verb.span(), |
| @@ -56,8 +56,8 @@ | |||
| 56 | 56 | /// without a production is a parse error naming it, which is the failure worth | |
| 57 | 57 | /// having. | |
| 58 | 58 | const MEMBERS: &[&str] = &[ | |
| 59 | - | "act", "across", "activate", "beside", "cell", "cells", "chip", "column", "field", "form", | |
| 60 | - | "given", "include", "link", "offering", "region", "row", "screen", "table", | |
| 59 | + | "act", "across", "activate", "at", "beside", "cell", "cells", "chip", "column", "field", | |
| 60 | + | "form", "given", "include", "link", "offering", "region", "row", "screen", "table", | |
| 61 | 61 | ]; | |
| 62 | 62 | ||
| 63 | 63 | /// The members that are one of `Node`'s own constructors, called by its name. | |
| @@ -95,6 +95,11 @@ | |||
| 95 | 95 | /// `badge` is the third and `chip` the fourth, and `chip` is not on this list | |
| 96 | 96 | /// because it carries an action: it has a parse arm of its own. Both accrete | |
| 97 | 97 | /// onto a `Tag` under `Container::Tag` and the emitter wraps `Node::token`. | |
| 98 | + | /// | |
| 99 | + | /// `timeline` is the fifth, and the only one whose body holds members rather | |
| 100 | + | /// than settings: an axis is told what sits on it and where, which is the `at` | |
| 101 | + | /// member under `Container::Timeline`. It is a `Node` either way, so nothing is | |
| 102 | + | /// wrapped afterwards. | |
| 98 | 103 | const NODE_MEMBERS: &[&str] = &[ | |
| 99 | 104 | "text", | |
| 100 | 105 | "toned", | |
| @@ -110,9 +115,12 @@ | |||
| 110 | 115 | "rich", | |
| 111 | 116 | "literal", | |
| 112 | 117 | "code", | |
| 118 | + | "since", | |
| 119 | + | "until", | |
| 113 | 120 | "picture", | |
| 114 | 121 | "canvas", | |
| 115 | 122 | "badge", | |
| 123 | + | "timeline", | |
| 116 | 124 | ]; | |
| 117 | 125 | ||
| 118 | 126 | /// The arrangements a screen may be laid out in. | |
| @@ -491,6 +499,22 @@ | |||
| 491 | 499 | }, | |
| 492 | 500 | }) | |
| 493 | 501 | } | |
| 502 | + | "at" => { | |
| 503 | + | let at: Arg = input.parse()?; | |
| 504 | + | let inner: Self = input.parse()?; | |
| 505 | + | // A guard on a placed entry guards the placing, exactly as | |
| 506 | + | // `beside`'s does: the timeline is what may hold nothing. | |
| 507 | + | Ok(match inner { | |
| 508 | + | Self::Guarded { guard, inner } => Self::Guarded { | |
| 509 | + | guard, | |
| 510 | + | inner: Box::new(Self::At { at, inner }), | |
| 511 | + | }, | |
| 512 | + | inner => Self::At { | |
| 513 | + | at, | |
| 514 | + | inner: Box::new(inner), | |
| 515 | + | }, | |
| 516 | + | }) | |
| 517 | + | } | |
| 494 | 518 | "region" => { | |
| 495 | 519 | let name: Arg = input.parse()?; | |
| 496 | 520 | input.parse::<Token![as]>()?; |
| @@ -8942,6 +8942,45 @@ | |||
| 8942 | 8942 | self | |
| 8943 | 8943 | } | |
| 8944 | 8944 | ||
| 8945 | + | /// An axis with nothing on it yet. | |
| 8946 | + | /// | |
| 8947 | + | /// [`Timeline`](Self::Timeline) was the last variant with no constructor of | |
| 8948 | + | /// its own, and its entries accrete for [`figure`](Self::figure)'s reason: | |
| 8949 | + | /// a caller building them one at a time, or conditionally, has nowhere to | |
| 8950 | + | /// hold the list. goingson's day view is the site. | |
| 8951 | + | #[must_use] | |
| 8952 | + | pub const fn timeline(track: layout::Track) -> Self { | |
| 8953 | + | Self::Timeline { | |
| 8954 | + | track, | |
| 8955 | + | entries: Vec::new(), | |
| 8956 | + | focus: None, | |
| 8957 | + | } | |
| 8958 | + | } | |
| 8959 | + | ||
| 8960 | + | /// One more thing on the axis, beside [`timeline`](Self::timeline). | |
| 8961 | + | /// | |
| 8962 | + | /// Does nothing to a node that is not a [`Timeline`](Self::Timeline), on | |
| 8963 | + | /// [`figure`](Self::figure)'s rule. | |
| 8964 | + | #[must_use] | |
| 8965 | + | pub fn placed(mut self, entry: Placed) -> Self { | |
| 8966 | + | if let Self::Timeline { entries, .. } = &mut self { | |
| 8967 | + | entries.push(entry); | |
| 8968 | + | } | |
| 8969 | + | self | |
| 8970 | + | } | |
| 8971 | + | ||
| 8972 | + | /// The moment the axis should bring into view, in minutes from its start. | |
| 8973 | + | /// | |
| 8974 | + | /// Does nothing to a node that is not a [`Timeline`](Self::Timeline), on | |
| 8975 | + | /// [`figure`](Self::figure)'s rule. | |
| 8976 | + | #[must_use] | |
| 8977 | + | pub fn focus(mut self, at: u16) -> Self { | |
| 8978 | + | if let Self::Timeline { focus, .. } = &mut self { | |
| 8979 | + | *focus = Some(at); | |
| 8980 | + | } | |
| 8981 | + | self | |
| 8982 | + | } | |
| 8983 | + | ||
| 8945 | 8984 | /// A tag standing on its own, rather than inside a row or a cell. | |
| 8946 | 8985 | /// | |
| 8947 | 8986 | /// [`Token`](Self::Token) was the last node variant with no constructor of |