Files
punktfunk/clients/windows/src/app/style.rs
T
enricobuehler 670f5a40a6 feat(clients/windows): screen-module restructure + parity features (speed test, native mode, capture UX)
Structure: split the 1400-line app.rs into per-screen app/ modules (mod=root/
router, hosts, connect, pair, speed, settings, licenses, stream, style) with
shared card/header/busy-page builders and setting_combo/toggle helpers; the
re-render rule (thread-driven state lives in root use_async_state, flows down
as props) is now documented at the module root.

Parity features the other clients already had:
- "Native display" resolves the real monitor mode at connect
  (MonitorFromWindow -> EnumDisplaySettingsW; was a hardcoded 1080p60)
- per-host network speed test: saved-host card button + a results screen
  (probe burst -> goodput/loss -> ~70% recommended bitrate applied in one
  tap; stale runs invalidated by generation) and `--headless --speed-test`;
  the bitrate setting becomes a free-form NumberBox so the recommendation
  round-trips
- forget host (ContentDialog confirm -> KnownHosts::remove_by_fp)
- settings: forwarded-controller picker (pads/pinned/set_pinned now wired),
  gamepad type, host compositor, capture-system-shortcuts; the previously
  dead Settings.compositor / inhibit_shortcuts are honored (shortcuts off =
  Alt+Tab/Alt+Esc/Ctrl+Esc/Win act locally)
- click-to-recapture after a Ctrl+Alt+Shift+Q release; the HUD hint tracks
  the live capture state

Perf: the input hook caches lock geometry (clip rect + contain-fit scale) at
engage instead of GetClientRect per WM_MOUSEMOVE; the audio jitter ring trims
via drain() and reuses the render scratch buffer.

Validated on the bare-metal box: --discover, synthetic-host loopback E2E
(TOFU -> clock skew -> HEVC negotiate -> D3D11VA init -> session end),
speed-test E2E, and the WinUI shell rendering in the console session via
PsExec (SSH/session-0 cannot create windows, pre-existing 0x80070005).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 12:16:19 +02:00

136 lines
4.3 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))
}
/// 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, 10.0, 0.0, 0.0))
.into()
}
/// Wrap a screen's children in a scrollable, centred, max-width column.
pub(crate) fn page(children: Vec<Element>) -> Element {
let col = vstack(children)
.spacing(10.0)
.max_width(640.0)
.horizontal_alignment(HorizontalAlignment::Center)
.margin(edges(24.0, 24.0, 24.0, 40.0));
scroll_view(col).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()
.horizontal_alignment(HorizontalAlignment::Center)
.into(),
text_block(detail)
.foreground(ThemeRef::SecondaryText)
.horizontal_alignment(HorizontalAlignment::Center)
.into(),
];
children.extend(extra);
vstack(children)
.spacing(16.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()
.foreground(ThemeRef::AccentText)
.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 {
Accent,
Good,
Neutral,
}
/// A small rounded status chip (paired/PIN/HDR/etc.).
pub(crate) fn pill(text: &str, kind: Pill) -> Border {
let (bg, fg) = match kind {
Pill::Accent => (ThemeRef::Accent, ThemeRef::AccentText),
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)
.corner_radius(10.0)
.padding(edges(9.0, 3.0, 9.0, 3.0))
}