//! Shared styling primitives for every screen, following the windows-reactor gallery's look: //! theme brushes (`ThemeRef`), rounded `border` cards, small all-caps section labels, and a //! centred max-width column per page. use windows_reactor::*; pub(crate) fn uniform(v: f64) -> Thickness { Thickness::uniform(v) } pub(crate) fn edges(left: f64, top: f64, right: f64, bottom: f64) -> Thickness { Thickness { left, top, right, bottom, } } /// A rounded, bordered surface in the theme's card colours. pub(crate) fn card(child: impl Into) -> Border { border(child.into()) .background(ThemeRef::CardBackground) .border_brush(ThemeRef::CardStroke) .border_thickness(uniform(1.0)) .corner_radius(8.0) .padding(uniform(16.0)) } /// Card chrome with no padding — for cards whose interactive regions (tap-to-connect area vs. /// action buttons) must own their padding so hit areas reach the card edges. pub(crate) fn card_flush(child: impl Into) -> Border { card(child).padding(uniform(0.0)) } /// An OPAQUE modal/dialog surface. `card`'s `CardBackground` is a translucent acrylic brush — fine /// layered on the page, but a floating dialog over a scrim needs a solid fill or the content behind /// bleeds through (looks "transparent"). `SolidBackground` is the opaque base-layer brush. pub(crate) fn dialog_surface(child: impl Into) -> Border { border(child.into()) .background(ThemeRef::SolidBackground) .border_brush(ThemeRef::SurfaceStroke) .border_thickness(uniform(1.0)) .corner_radius(8.0) .padding(uniform(20.0)) } /// A fully transparent brush: paints nothing but (unlike a null background) makes the whole /// element hit-testable, so a tap region catches clicks in its blank space too. pub(crate) fn hit_test_backstop() -> Color { Color { a: 0, r: 0, g: 0, b: 0, } } /// A small all-caps section label above a group of cards. pub(crate) fn section(label: &str) -> Element { text_block(label) .font_size(12.0) .semibold() .foreground(ThemeRef::SecondaryText) .margin(edges(2.0, 14.0, 0.0, 2.0)) .into() } /// Wrap a screen's children in a scrollable, centred, max-width column. Alignment stays the /// default Stretch: with a MaxWidth that still centres the column, but the children get the /// column's REAL width — an explicit Center would size the column to its content and leave /// every card at its minimum width no matter how large the window is. pub(crate) fn page(children: Vec) -> Element { let col = vstack(children) .spacing(10.0) .max_width(640.0) .margin(edges(24.0, 24.0, 24.0, 40.0)); scroll_view(col).into() } /// Like [`page`], but wide and airier — for screens whose cards lay out in a responsive grid /// and should use the window instead of a narrow settings column. pub(crate) fn page_wide(children: Vec) -> Element { let col = vstack(children) .spacing(14.0) .max_width(1120.0) .margin(edges(32.0, 28.0, 32.0, 48.0)); scroll_view(col).into() } /// Lay tiles into a `cols`-wide grid of equal-width star columns (rows share the height of /// their tallest tile, so a grid row always lines up). Shared by the hosts page's host /// tiles and the library page's posters. pub(crate) fn tile_grid(tiles: Vec, cols: usize, gap: f64) -> Element { let rows = tiles.len().div_ceil(cols); let mut children = Vec::with_capacity(tiles.len()); for (i, t) in tiles.into_iter().enumerate() { children.push(t.grid_row((i / cols) as i32).grid_column((i % cols) as i32)); } grid(children) .columns(vec![GridLength::Star(1.0); cols]) .rows(vec![GridLength::Auto; rows]) .column_spacing(gap) .row_spacing(gap) .into() } /// A page header: a large bold title on the left, one action button on the right. pub(crate) fn page_header(title: &str, action: Button) -> Element { grid(( text_block(title) .font_size(30.0) .bold() .grid_column(0) .vertical_alignment(VerticalAlignment::Center), action .grid_column(1) .vertical_alignment(VerticalAlignment::Center), )) .columns([GridLength::Star(1.0), GridLength::Auto]) .margin(edges(0.0, 0.0, 0.0, 6.0)) .into() } /// A full-screen centred "busy" scene: spinner, headline, secondary detail line, and optional /// trailing elements (e.g. a Cancel button). Shared by Connecting / RequestAccess / SpeedTest. pub(crate) fn busy_page(headline: &str, detail: &str, extra: Vec) -> Element { let mut children: Vec = vec![ ProgressRing::indeterminate() .width(48.0) .height(48.0) .horizontal_alignment(HorizontalAlignment::Center) .into(), text_block(headline) .font_size(18.0) .semibold() .wrap() .horizontal_alignment(HorizontalAlignment::Center) .into(), text_block(detail) .wrap() .foreground(ThemeRef::SecondaryText) .horizontal_alignment(HorizontalAlignment::Center) .into(), ]; children.extend(extra); // max_width + side margins so the text column reads well wide AND wraps instead of // clipping narrow. vstack(children) .spacing(16.0) .max_width(520.0) .margin(edges(24.0, 0.0, 24.0, 0.0)) .horizontal_alignment(HorizontalAlignment::Center) .vertical_alignment(VerticalAlignment::Center) .into() } /// A rounded square "monogram" for a host, the first letter on an accent fill — a clean leading /// visual that avoids depending on an icon font being installed. pub(crate) fn avatar(name: &str) -> Border { let initial = name .chars() .find(|c| c.is_alphanumeric()) .map(|c| c.to_uppercase().to_string()) .unwrap_or_else(|| "?".into()); border( text_block(initial) .font_size(17.0) .semibold() // NOT ThemeRef::AccentText — that's accent-COLOURED text for normal surfaces; // on an accent fill it's accent-on-accent (unreadable). This is the on-accent brush. .foreground(ThemeRef::custom("TextOnAccentFillColorPrimaryBrush")) .horizontal_alignment(HorizontalAlignment::Center) .vertical_alignment(VerticalAlignment::Center), ) .background(ThemeRef::Accent) .corner_radius(10.0) .width(40.0) .height(40.0) } /// Pill chip colour intent. #[derive(Clone, Copy)] pub(crate) enum Pill { Info, Good, Neutral, } /// A small rounded status chip (paired/PIN/HDR/etc.) — subtle tinted fills with matching /// system foregrounds (the InfoBar palette), never solid accent (white-on-bright is unreadable). pub(crate) fn pill(text: &str, kind: Pill) -> Border { let (bg, fg) = match kind { Pill::Info => ( ThemeRef::SystemAttentionBackground, ThemeRef::SystemAttention, ), Pill::Good => (ThemeRef::SystemSuccessBackground, ThemeRef::SystemSuccess), Pill::Neutral => (ThemeRef::SubtleFill, ThemeRef::SecondaryText), }; border(text_block(text).font_size(11.0).semibold().foreground(fg)) .background(bg) .border_brush(ThemeRef::CardStroke) .border_thickness(uniform(1.0)) .corner_radius(10.0) .padding(edges(9.0, 2.0, 9.0, 2.0)) } /// A small presence dot (host online/offline). pub(crate) fn presence_dot(online: bool) -> Border { border(vstack(Vec::::new())) .background(if online { ThemeRef::SystemSuccess } else { ThemeRef::SystemNeutral }) .corner_radius(4.0) .width(8.0) .height(8.0) }