Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions docs/codebase-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ Daemon mode therefore provides a persistent background service that reacts to us
- Communicate with `capture::CaptureManager` for screenshot actions.
- Exit when `InputState.should_exit` is set (Escape, tray close, etc.).

`WaylandState` coordinates the runtime owners handlers need. `FocusState` owns activation, focus, and startup acquisition; `ProtocolGlobals` owns bound globals and toolkit handler state; `PointerRuntime` owns pointer position, board-pan and chrome gestures, cursor, pointer-lock, and single-contact touch protocol lifecycles; `ToolbarChrome` owns toolbar placement, inline interaction, and fade state; `ToolbarDrag` owns built-in and GTK drag lifecycles; `RegionCaptureRuntime` owns region selection generations, active/review/window-snap state, and the window-query and cut-preview workers; `AcquisitionRuntime` owns the capacity-one screen-acquisition and zoom-waiter registries plus eyedropper source correlation; `FrozenState` owns its availability and one-shot startup gate; `SurfaceState` owns output/fullscreen/layer placement and frozen-fullscreen transitions; `OverlaySuppressionState` owns suppression reason, keyboard policy, capture barrier, and clickthrough state; `RenderRuntime` owns the canvas layer cache, reusable image and blur caches, render-profile baseline, and per-effect damage history; `InputHudRuntime` owns system-reader lifecycle and reconciliation; `SpotlightRuntime` owns render memory, warning latches, and wheel timing; `ClipboardRuntime` owns single-flight clipboard workers and queue policy; `PreferenceStores` groups durable preference stores and workers; `UiAnimationClock` owns animation scheduling; and `FontCatalogPrewarm` owns the one-shot font scan. The root retains cross-owner coordination. `handlers::route::SurfaceRouter` is the single classifier for pointer, touch, and stylus surfaces and supplies overlay screen coordinates before modality-specific dispatch.
`WaylandState` coordinates the runtime owners handlers need. `FocusState` owns activation, focus, and startup acquisition; `ProtocolGlobals` owns bound globals and toolkit handler state; `PointerRuntime` owns pointer position, board-pan and chrome gestures, cursor, pointer-lock, and single-contact touch protocol lifecycles; `ToolbarChrome` owns toolbar placement, inline interaction, and fade state; `ToolbarDrag` owns built-in and GTK drag lifecycles; `RegionCaptureRuntime` owns region selection generations, active/review/window-snap state, and the window-query and cut-preview workers; `AcquisitionRuntime` owns the capacity-one screen-acquisition and zoom-waiter registries plus eyedropper source correlation; `FrozenState` owns its availability and one-shot startup gate; `SurfaceState` owns output/fullscreen/layer placement and frozen-fullscreen transitions; `OverlaySuppressionState` owns suppression reason, keyboard policy, capture barrier, and clickthrough state; `RenderRuntime` owns the canvas layer cache, reusable image and blur caches, resolved theme, UI paint caches, render-profile baseline, and per-effect damage history; `InputHudRuntime` owns system-reader lifecycle and reconciliation; `SpotlightRuntime` owns render memory, warning latches, and wheel timing; `ClipboardRuntime` owns single-flight clipboard workers and queue policy; `PreferenceStores` groups durable preference stores and workers; `UiAnimationClock` owns animation scheduling; and `FontCatalogPrewarm` owns the one-shot font scan. The root retains cross-owner coordination. `handlers::route::SurfaceRouter` is the single classifier for pointer, touch, and stylus surfaces and supplies overlay screen coordinates before modality-specific dispatch.

Within `PointerRuntime`, pending chrome targets and device-owned release suppression have separate lifecycles. Clearing a toast, HUD, or zoom-chip press preserves mouse and touch suppression; cancelling a touch clears only its own release latch.

Expand Down Expand Up @@ -147,13 +147,19 @@ Freeze capture waits for the overlay-suppression frame, then selects `wlr-screen
defers the frame before preparation. `state/render/{prepare,plan,paint,submit}.rs` keep
these boundaries explicit; the planner derives canvas policy and screen/world/buffer damage.
- `CanvasRenderCtx` borrows frame parameters and the local Cairo target. `RenderRuntime`
owns the canvas layer cache, drawing resources, reusable profile baseline, and effect
damage history. Drawing resources stay outside the owned `FramePlan`.
owns the canvas layer cache, drawing resources, resolved theme, UI paint caches,
reusable profile baseline, and effect damage history. These resources stay outside
the owned `FramePlan`.
- `draw::RenderCtx` borrows a Cairo target and explicit `RenderCaches` for image and blur
rendering. The overlay shares its cache owner across direct drawing, layer baking,
provisional previews, and board thumbnails. Each export job creates its own owner;
PDF pages and magnified-page raster passes share that job's image cache. Export blur
remains uncached because page backdrops can share a numeric source identifier.
- `ui::UiRenderCtx` borrows the Cairo target, resolved theme, and `UiRenderCaches` for
help and radial painting. Help layout and radial surfaces retain their one-entry
policies; radial reuse also compares theme values and the actual Cairo matrix scale.
Status and zoom painting borrow only the theme they need. The About dialog resolves
and owns its own theme when it starts. Legacy popup colors retain their fixed palette.
- Draw order: board background → finalized shapes → spotlight effects → provisional shape
→ text cursor preview → UI. Color-profile passes surround UI painting according to their
selected targets. Main-surface submission precedes toolbar rendering and marking capture
Expand All @@ -169,6 +175,14 @@ local drawing resources, as do `InputState::render_provisional_shape` and
owner. Public PNG/PDF export functions keep their snapshot-based interfaces and create
Cairo resources inside the rendering job, after any worker-thread handoff.

The public `ui::render_help_overlay` and `ui::render_radial_menu` wrappers create local
UI cache owners. Only the legacy radial, status-bar, and zoom-chip wrappers read
`ui::theme::current()`, preserving standalone callers that use `ui::theme::init()`.
Those compatibility APIs remain until a deliberate public API migration; new internal
callers use explicit contexts or themes. Overlay and About startup neither initialize
nor read the compatibility theme. Help hit geometry and text measurement retain their
separate lifecycles.

---

## 5. Capture Pipeline
Expand Down
1 change: 1 addition & 0 deletions src/about_window/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
- Preserve foreground/fullscreen safety for any launch, focus, or window behavior.
- Keep protocol handlers thin and rendering deterministic.
- Chrome colors come from `crate::ui::theme`, never hardcoded literals.
- Resolve the configured theme at startup and retain it on the About owner; painting borrows that value independently of the overlay and legacy public theme compatibility state.
- Every outbound link points at wayscriber.com (no code-host links); the update
card never installs anything.
- Nothing leaves the machine without the user asking. "Report a problem" copies
Expand Down
10 changes: 6 additions & 4 deletions src/about_window/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,13 @@ use layout::Plan;
pub fn run_about_window() -> Result<()> {
// Chrome colors come from the same `[ui] theme` key as the overlay, so the
// dialog matches the toolbars the user already sees.
match Config::load() {
Ok(loaded) => crate::ui::theme::init(loaded.config.ui.theme.to_theme_mode()),
let theme = match Config::load() {
Ok(loaded) => crate::ui::theme::Theme::resolve(loaded.config.ui.theme.to_theme_mode()),
Err(err) => {
debug!("About dialog falling back to the default theme: {err}");
crate::ui::theme::init(Config::default().ui.theme.to_theme_mode());
crate::ui::theme::Theme::resolve(Config::default().ui.theme.to_theme_mode())
}
}
};

let conn = Connection::connect_to_env().context("Failed to connect to Wayland compositor")?;
let (globals, mut event_queue) =
Expand Down Expand Up @@ -82,6 +82,7 @@ pub fn run_about_window() -> Result<()> {
window,
content,
plan,
theme,
);

// Join helpers on every return path so ProcessBrokerGuard teardown cannot
Expand Down Expand Up @@ -128,6 +129,7 @@ fn surface_size(plan: &Plan) -> (u32, u32) {
}

struct AboutWindowState {
theme: crate::ui::theme::Theme,
registry_state: RegistryState,
compositor_state: CompositorState,
shm: Shm,
Expand Down
43 changes: 37 additions & 6 deletions src/about_window/render/draw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,7 @@ impl Frame<'_> {
}
}

pub(super) fn draw_about(ctx: &cairo::Context, frame: &Frame<'_>) {
let theme = theme::current();

pub(super) fn draw_about(ctx: &cairo::Context, theme: &Theme, frame: &Frame<'_>) {
backdrop(ctx, frame.plan, theme);
header(ctx, frame, theme);
update_card(ctx, frame, theme);
Expand Down Expand Up @@ -437,6 +435,39 @@ mod tests {
cairo::Context::new(&surface).unwrap()
}

#[test]
fn about_paints_each_explicit_theme_without_changing_plan() {
let content = AboutContent::build();
let plan = layout::plan(&content);
let update = UpdateState::Checking;
let frame = frame_for(&plan, &content, &update);
let paint = |theme: &Theme| {
let mut surface = cairo::ImageSurface::create(
cairo::Format::ARgb32,
plan.width.ceil() as i32,
plan.height.ceil() as i32,
)
.unwrap();
{
let ctx = cairo::Context::new(&surface).unwrap();
draw_about(&ctx, theme, &frame);
assert_eq!(ctx.status(), Ok(()));
}
surface.flush();
surface.data().unwrap().to_vec()
};
let dark = paint(&Theme::dark());
let light = paint(&Theme::light());
assert!(
dark != light,
"explicit About themes must change chrome colors"
);
assert!(
dark == paint(&Theme::dark()),
"another theme must not replace the first owner's theme"
);
}

/// The dialog is a fixed width, so row wording has to be chosen to fit it.
/// An ellipsis here means a row's text was written without checking.
#[test]
Expand Down Expand Up @@ -499,7 +530,7 @@ mod tests {
];

for state in &states {
draw_about(&ctx, &frame_for(&plan, &content, state));
draw_about(&ctx, &Theme::dark(), &frame_for(&plan, &content, state));
assert_eq!(ctx.status(), Ok(()), "state {state:?} failed to paint");
}
}
Expand All @@ -515,12 +546,12 @@ mod tests {
frame.hover = Some(Element::Link(0));
frame.focus = Some(Element::Close);
frame.notice = Some("Copied to clipboard");
draw_about(&ctx, &frame);
draw_about(&ctx, &Theme::dark(), &frame);

frame.hover = Some(Element::UpdateCard);
frame.focus = Some(Element::Button(0));
frame.notice = None;
draw_about(&ctx, &frame);
draw_about(&ctx, &Theme::dark(), &frame);

assert_eq!(ctx.status(), Ok(()));
}
Expand Down
1 change: 1 addition & 0 deletions src/about_window/render/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ impl AboutWindowState {

draw::draw_about(
&ctx,
&self.theme,
&draw::Frame {
plan: &self.plan,
content: &self.content,
Expand Down
2 changes: 2 additions & 0 deletions src/about_window/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ impl AboutWindowState {
window: super::Window,
content: AboutContent,
plan: Plan,
theme: crate::ui::theme::Theme,
) -> Self {
// Opening the dialog costs no network: the row reports whatever the
// last background check wrote, and the user can ask for a fresh one.
Expand All @@ -36,6 +37,7 @@ impl AboutWindowState {
let (width, height) = surface_size(&plan);

Self {
theme,
registry_state,
compositor_state,
shm,
Expand Down
4 changes: 0 additions & 4 deletions src/backend/wayland/backend/state_init/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,6 @@ pub(super) fn load(backend_exit_mode: ExitAfterCaptureMode) -> LoadedConfig {
}
};

// Install process-wide UI preferences before any surface renders. The
// daemon spawns fresh overlay processes that re-enter this load path, so
// this single call site covers both direct and daemon-managed overlays.
crate::ui::theme::init(config.ui.theme.to_theme_mode());
crate::ui::anim::set_motion_enabled(config.ui.reduced_motion.motion_enabled());

let exit_after_capture_mode = match backend_exit_mode {
Expand Down
2 changes: 1 addition & 1 deletion src/backend/wayland/state/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
- This subtree supports live overlay runtime state: buffers, damage, boards, capture routing, clipboard paste, color picker, onboarding, PDF export, render helpers, toolbar plumbing, zoom, and core accessors.
- Runtime owners extracted from `WaylandState` live beside it: `focus.rs` (activation, focus, and startup acquisition), `protocol_globals.rs` (bound globals and toolkit handler state), `pointer_runtime.rs` (pointer position, board-pan and chrome gestures, cursor, pointer-lock, and touch lifecycles), `region_capture/runtime.rs` (region generations, active/review/window-snap state, and query/preview workers), `acquisition.rs` (screen acquisition, zoom waiters, and eyedropper source correlation), `input_hud.rs` (system-reader lifecycle and reconciliation), `spotlight_runtime.rs` (render memory, warning latches, and wheel timing), `clipboard_runtime.rs` (single-flight workers and queue policy), `preference_stores.rs` (persistence stores and workers), `ui_animation.rs` (animation scheduling), `font_catalog.rs` (font-catalog prewarm), `text_input.rs` (text-input-v3 lifecycle and commit serials), `tablet_runtime.rs` (tablet-input-v2 objects and stylus contact), `key_repeat.rs` (manual key-repeat timing), and `helper_launch.rs` (About/configurator launches requested by input).
- `core/overlay.rs` owns suppression policy and capture-barrier state; `../surface.rs` owns output/fullscreen/layer placement and frozen-fullscreen transitions.
- `render/` separates mutable preparation, pure `FramePlan` decisions, Cairo painting, and buffer submission. `CanvasRenderCtx` borrows frame parameters; `RenderRuntime` owns the canvas layer cache, explicit drawing caches, profile baseline, and per-effect damage history. Direct canvas rendering, layer baking, previews, and board thumbnails borrow that drawing owner in short scopes. `toolbar/` owns `ToolbarChrome` (placement, inline interaction, and fade state), `ToolbarDrag` (built-in and GTK drag lifecycles), and runtime toolbar effects; `clipboard/` owns session paste helpers.
- `render/` separates mutable preparation, pure `FramePlan` decisions, Cairo painting, and buffer submission. `CanvasRenderCtx` borrows frame parameters; `RenderRuntime` owns the canvas layer cache, explicit drawing caches, resolved theme, UI paint caches, profile baseline, and per-effect damage history. Direct canvas rendering, layer baking, previews, and board thumbnails borrow that drawing owner in short scopes. `toolbar/` owns `ToolbarChrome` (placement, inline interaction, and fade state), `ToolbarDrag` (built-in and GTK drag lifecycles), and runtime toolbar effects; `clipboard/` owns session paste helpers.

## Invariants
- Preserve snapshot boundaries for export and session actions.
Expand Down
4 changes: 3 additions & 1 deletion src/backend/wayland/state/core/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,9 @@ impl WaylandState {
toolbar: ToolbarSurfaceManager::new(),
toolbar_chrome,
toolbar_drag: super::super::toolbar::ToolbarDrag::new(),
render: super::super::render::RenderRuntime::new(),
render: super::super::render::RenderRuntime::new(crate::ui::theme::Theme::resolve(
config.ui.theme.to_theme_mode(),
)),
suppression: Default::default(),
shortcut_coach: Default::default(),
focus: super::super::focus::FocusState::new(startup_activation_token),
Expand Down
31 changes: 30 additions & 1 deletion src/backend/wayland/state/render/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,20 +120,34 @@ impl UiDamageHistory {
pub(in crate::backend::wayland) struct RenderRuntime {
canvas_layer_cache: CanvasLayerCache,
draw_caches: crate::draw::RenderCaches,
theme: crate::ui::theme::Theme,
ui_caches: crate::ui::UiRenderCaches,
ui_damage: UiDamageHistory,
profile_ui_baseline: Vec<u8>,
}

impl RenderRuntime {
pub(in crate::backend::wayland) fn new() -> Self {
pub(in crate::backend::wayland) fn new(theme: crate::ui::theme::Theme) -> Self {
Self {
canvas_layer_cache: CanvasLayerCache::new(),
draw_caches: crate::draw::RenderCaches::default(),
theme,
ui_caches: crate::ui::UiRenderCaches::default(),
ui_damage: UiDamageHistory::default(),
profile_ui_baseline: Vec::new(),
}
}

pub(in crate::backend::wayland::state) fn theme(&self) -> &crate::ui::theme::Theme {
&self.theme
}

pub(in crate::backend::wayland::state) fn ui_parts_mut(
&mut self,
) -> (&crate::ui::theme::Theme, &mut crate::ui::UiRenderCaches) {
(&self.theme, &mut self.ui_caches)
}

pub(in crate::backend::wayland::state) fn canvas_layer_cache_mut(
&mut self,
) -> &mut CanvasLayerCache {
Expand Down Expand Up @@ -171,6 +185,21 @@ mod tests {
use super::*;
use crate::backend::wayland::state::buffer_damage::{BufferDamageTracker, FullDamageReason};

#[test]
fn runtime_themes_are_independent_of_other_owners() {
let mut dark = RenderRuntime::new(crate::ui::theme::Theme::resolve(
crate::ui::theme::ThemeMode::Dark,
));
let light = RenderRuntime::new(crate::ui::theme::Theme::resolve(
crate::ui::theme::ThemeMode::Light,
));
assert_eq!(dark.theme(), &crate::ui::theme::Theme::dark());
assert_eq!(light.theme(), &crate::ui::theme::Theme::light());
let (theme, _caches) = dark.ui_parts_mut();
assert_eq!(theme, &crate::ui::theme::Theme::dark());
assert_ne!(dark.theme(), light.theme());
}

fn rect(x: i32) -> Rect {
Rect::new(x, 0, 10, 10).expect("test rectangle")
}
Expand Down
Loading
Loading