Files
punktfunk/clients/windows/src/app/style.rs
T
enricobuehler f4850625bd
apple / swift (push) Successful in 1m10s
android / android (push) Successful in 4m56s
arch / build-publish (push) Successful in 5m56s
audit / bun-audit (push) Successful in 13s
audit / cargo-audit (push) Successful in 1m34s
ci / web (push) Successful in 1m11s
windows-host / package (push) Successful in 7m57s
ci / docs-site (push) Successful in 1m27s
release / apple (push) Successful in 8m39s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m26s
ci / bench (push) Successful in 5m0s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m16s
apple / screenshots (push) Successful in 5m35s
ci / rust (push) Successful in 10m32s
decky / build-publish (push) Successful in 13s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 5s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 7s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 4m47s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 2m36s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 5s
deb / build-publish (push) Successful in 6m38s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m39s
flatpak / build-publish (push) Successful in 5m32s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 11m0s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 10m33s
docker / deploy-docs (push) Successful in 18s
feat(clients/windows): game library page + session window placement + help polish
UX polish batch 2 (plan workstreams D/E5/E6):

- D: a mouse/keyboard game library page over pf-client-core::library (the GTK
  ui_library.rs counterpart): responsive 2-6 column poster grid (2:3 portraits,
  store badge, monogram placeholder while art streams in), loading / error+retry
  / empty / grid states, tap-to-play via a normal spawn carrying --launch id.
  Poster bytes land in a disk cache (%LOCALAPPDATA%\punktfunk\art-cache) so the
  Image widget loads file:/// URIs (reactor has no from-bytes source) and
  revisits render instantly. Reached from a paired host's "Browse library..."
  menu item behind the new "Show game library (experimental)" Settings toggle
  (the shared library_enabled field, Apple/GTK parity). A Shared::library_gen
  generation guard keeps a superseded fetch from publishing (speed-test pattern).

- E5: the session window opens at the shell's own top-left (--window-pos, new
  SessionOpts::window_pos) instead of centered on the primary display - streams
  land on the monitor the shell is on, and the hide/restore handoff reads as one
  window changing content. Fullscreen follows that display.

- E6: the Help shortcuts add Alt+Enter and the controller escape chord
  (LB+RB+Start+Back, hold to disconnect).

Also: rustfmt normalization of the merged probe helper.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 00:43:46 +02:00

221 lines
7.8 KiB
Rust

//! 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<Element>) -> 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<Element>) -> 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<Element>) -> 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>) -> 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>) -> 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<Element>, 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>) -> Element {
let mut children: Vec<Element> = 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::<Element>::new()))
.background(if online {
ThemeRef::SystemSuccess
} else {
ThemeRef::SystemNeutral
})
.corner_radius(4.0)
.width(8.0)
.height(8.0)
}