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
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>
221 lines
7.8 KiB
Rust
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)
|
|
}
|