//! A dimension a set is narrowed by, rendered as a list of values. //! //! The fourth phase-B emitter, beside [`form`](crate::form), //! [`list`](crate::list) and [`meter`](crate::meter). Same split as those: this //! owns the structure of the panel and the app owns the routes. A value's //! identifier leaves in `data-facet-value`, which is the hook an app wires its //! own request onto, exactly as [`list`](crate::list) writes `data-column` and //! lets the app decide what pressing a heading calls. //! //! # Why this is markup and not only CSS //! //! Phase A's rule is that an app keeps its markup and gains the classes, and //! that rule works because the markup already existed. Here it mostly does not: //! a facet panel is the shape MNW's discover page reached by writing a tick box //! and a chevron per row because filtering and browsing were two mechanisms, and //! the whole point of [`makeover_layout::Selecting::Subtree`] is that they stop //! being two. There is nothing to keep. //! //! # The one thing drawn that no flat control has //! //! An exclude affordance beside each value, in a subtree facet only. It is a //! visible control rather than a modifier or a long press, and that was ruled //! rather than chosen here: a gesture a terminal cannot express is a gesture //! half the renderers leave out, and an affordance nothing teaches is one users //! do not find. The glyph is this renderer's pick, and it takes the standing //! preference for the heavier, simpler mark. //! //! # What the depth does and does not do //! //! `--facet-depth` carries the tree level as a number, and the indent rule //! multiplies it by one geometry step. That keeps the whole tree one flat list //! in the DOM rather than nested lists, which is what lets a renderer draw the //! same description as a breadcrumb or a column of panes without the markup //! disagreeing. It is not a size: the number is the level, and the step is //! `makeover-geometry`'s. use crate::form::escape_into; use crate::reset::Reset; use crate::{Emit, class, push_class}; use makeover_layout::{Depth, Facet, FacetValue, Selecting, Standing}; use std::fmt::Write as _; /// The classes this module can put in markup. /// /// [`crate::list::ROW_PART_CLASSES`]' obligation, and it exists for the same /// reason: every class here is also ruled by [`facet_rules`], so the vocabulary /// seal picks them up from the generated sheet, and this list is what a test /// checks that against. pub const FACET_CLASSES: &[&str] = &[ "facet", "facet-name", "facet-values", "facet-value", "facet-take", "facet-count", "facet-prune", ]; /// The name a selection mode goes by in `data-selecting`. /// /// An attribute rather than a class, for `data-selector`'s reason on a selector /// group: the mode changes what the panel *means*, not how one value is /// painted, and a class there would read as the styling hook the value's class /// actually is. #[must_use] pub const fn selecting_name(mode: Selecting) -> &'static str { match mode { Selecting::OneOf => "one-of", Selecting::AnyOf => "any-of", Selecting::Range => "range", Selecting::Text => "text", Selecting::Subtree => "subtree", // A mode added to the description since this renderer was built. // `Selecting` is `#[non_exhaustive]`, and an unknown mode reads as the // one that offers no values and prunes nothing: drawing a value list // for a mode whose values mean something else is the worse mistake. _ => "unknown", } } /// The name a standing goes by in `data-standing`. #[must_use] pub const fn standing_name(standing: Standing) -> &'static str { match standing { Standing::Open => "open", Standing::Taken => "taken", Standing::Inherited => "inherited", Standing::Pruned => "pruned", // Unknown reads as open, which is the state that claims nothing about // the set. _ => "open", } } /// A facet as a labelled list of values. /// /// ``` /// use makeover_layout::{Facet, FacetValue, Selecting, Standing}; /// use makeover_webview::{Emit, facet::facet_html}; /// /// let values = [ /// FacetValue::new("music", "Music") /// .standing(Standing::Taken) /// .counted(128) /// .at(0, true), /// FacetValue::new("music/synths", "Synths").at(1, false), /// ]; /// let facet = Facet::new("Tag", Selecting::Subtree, &values); /// let html = facet_html(&facet, &Emit::default()); /// /// assert!(html.contains(r#"data-selecting="subtree""#)); /// assert!(html.contains(r#"data-facet-value="music/synths""#)); /// // A subtree is the one mode that offers a way to prune a branch out. /// assert!(html.contains("facet-prune")); /// ``` /// /// A [`Selecting::Text`] or [`Selecting::Range`] facet lists nothing, so what /// comes back is the panel and its name with an empty list inside it. That is /// deliberate rather than an empty string: the app puts its own box in the /// panel, and the panel is what gives the box the group label and the shared /// geometry. #[must_use] pub fn facet_html(facet: &Facet<'_>, opts: &Emit) -> String { let mut html = String::new(); facet_html_into(facet, opts, &mut html); html } /// A facet, written into a buffer the caller already has. /// /// [`facet_html`]'s streaming form, byte-identical to it. pub fn facet_html_into(facet: &Facet<'_>, opts: &Emit, out: &mut String) { out.push_str("
", facet.reach()); out.push_str("

"); escape_into(facet.name, out); out.push_str("

"); out.push_str("
"); } fn value_html_into(facet: &Facet<'_>, value: &FacetValue<'_>, opts: &Emit, out: &mut String) { out.push_str("
  • ", value.depth); out.push_str(""); if facet.mode.prunes() { out.push_str(""); } out.push_str("
  • "); } /// The rules for a facet panel. /// /// Depth comes from the description: a value at rest sits as /// [`Depth::Flat`] and a taken one is held in, which is /// [`makeover_layout::Selector::chosen`]'s shape for a segment and is the same /// sentence — this one is picked, so it is pressed. Nothing here states a /// colour or a size; the indent is a count multiplied by a geometry step, and /// the step is the one variable this crate is allowed to read. pub(crate) fn facet_rules(opts: &Emit) -> String { let mut css = String::new(); let panel = class("facet", opts); let name = class("facet-name", opts); let values = class("facet-values", opts); let value = class("facet-value", opts); let take = class("facet-take", opts); let count = class("facet-count", opts); let prune = class("facet-prune", opts); // The name of the dimension. A caption, and captions are legitimately // muted: it was never going to answer a press. let _ = writeln!(css, ".{name} {{\n color: var(--content-muted);\n}}"); // The list gives back what a `