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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ For distro-specific package details, see [Installation](#installation). For keyb
- Selection: Alt-drag, <kbd>V</kbd> tool, properties panel
- Duplicate (<kbd>Ctrl+D</kbd>), delete (<kbd>Delete</kbd>), undo/redo
- Color picker, palettes, size via hotkeys or scroll
- Render color profiles for print/projector/light-theme preview
- Radial menu at cursor (<kbd>Middle-click</kbd>): quick tool/color selection + scroll size adjust

### Boards
Expand Down Expand Up @@ -915,6 +916,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, project structure,
- [x] Highlighter & eraser tools
- [x] Additional shapes (filled shapes)
- [x] Color picker
- [x] Render color profiles
- [x] Zoom (ZoomIt-style controls)
- [x] Presets (tool/color/size slots)
- [x] Sticky notes
Expand Down
30 changes: 30 additions & 0 deletions config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ toggle_light_mode = ["Ctrl+Shift+L"]
# or `--light-draw-on` / `--light-draw-off` from a compositor shortcut.
toggle_light_mode_drawing = []

# Optional render color profile preview controls
render_profile_next = []
render_profile_previous = []
render_profile_off = []

# Toggle fill for rectangle/ellipse
toggle_fill = []

Expand Down Expand Up @@ -606,6 +611,31 @@ auto_adjust_pen = true
# default_pen_color = { rgb = [0.29, 0.23, 0.18] }
# auto_adjust_pen = true

# ═══════════════════════════════════════════════════════════════════════════════
# RENDER COLOR PROFILES
# ═══════════════════════════════════════════════════════════════════════════════

[render_profiles]
# Optional profile id to preview on startup
# active = "print"
# Apply profiles to board backgrounds, annotations, and canvas-space previews
apply_to_canvas = true
# Apply profiles to screen-space UI chrome, toolbars, popups, and status text
apply_to_ui = true

# Render profiles remap final rendered pixels by exact RGB match. Alpha is
# preserved, and colors not listed here are unchanged.
#
# [[render_profiles.items]]
# id = "print"
# name = "Print"
# mappings = [
# { from = "#000000", to = "#FFFFFF" },
# { from = "#FFFFFF", to = "#000000" },
# { from = "#FFFF00", to = "#8B4513" },
# { from = "#00FF00", to = "#006400" },
# ]

# ═══════════════════════════════════════════════════════════════════════════════
# SESSION PERSISTENCE
# ═══════════════════════════════════════════════════════════════════════════════
Expand Down
3 changes: 3 additions & 0 deletions configurator/src/models/keybindings/field/config/read.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ impl KeybindingField {
Self::ToggleClickHighlight => &config.ui.toggle_click_highlight,
Self::ToggleToolbar => &config.ui.toggle_toolbar,
Self::TogglePresenterMode => &config.ui.toggle_presenter_mode,
Self::RenderProfileNext => &config.ui.render_profile_next,
Self::RenderProfilePrevious => &config.ui.render_profile_previous,
Self::RenderProfileOff => &config.ui.render_profile_off,
Self::ToggleFill => &config.ui.toggle_fill,
Self::ToggleHighlightTool => &config.tools.toggle_highlight_tool,
Self::ToggleSelectionProperties => &config.ui.toggle_selection_properties,
Expand Down
3 changes: 3 additions & 0 deletions configurator/src/models/keybindings/field/config/write.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ impl KeybindingField {
Self::ToggleClickHighlight => config.ui.toggle_click_highlight = value,
Self::ToggleToolbar => config.ui.toggle_toolbar = value,
Self::TogglePresenterMode => config.ui.toggle_presenter_mode = value,
Self::RenderProfileNext => config.ui.render_profile_next = value,
Self::RenderProfilePrevious => config.ui.render_profile_previous = value,
Self::RenderProfileOff => config.ui.render_profile_off = value,
Self::ToggleFill => config.ui.toggle_fill = value,
Self::ToggleHighlightTool => config.tools.toggle_highlight_tool = value,
Self::ToggleSelectionProperties => config.ui.toggle_selection_properties = value,
Expand Down
6 changes: 6 additions & 0 deletions configurator/src/models/keybindings/field/labels.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ impl KeybindingField {
Self::ToggleClickHighlight => "Toggle click highlight",
Self::ToggleToolbar => "Toggle toolbar",
Self::TogglePresenterMode => "Toggle presenter mode",
Self::RenderProfileNext => "Render profile: next",
Self::RenderProfilePrevious => "Render profile: previous",
Self::RenderProfileOff => "Render profile: off",
Self::ToggleFill => "Toggle fill",
Self::ToggleHighlightTool => "Toggle highlight tool",
Self::ToggleSelectionProperties => "Toggle selection properties",
Expand Down Expand Up @@ -203,6 +206,9 @@ impl KeybindingField {
Self::ToggleClickHighlight => "toggle_click_highlight",
Self::ToggleToolbar => "toggle_toolbar",
Self::TogglePresenterMode => "toggle_presenter_mode",
Self::RenderProfileNext => "render_profile_next",
Self::RenderProfilePrevious => "render_profile_previous",
Self::RenderProfileOff => "render_profile_off",
Self::ToggleFill => "toggle_fill",
Self::ToggleHighlightTool => "toggle_highlight_tool",
Self::ToggleSelectionProperties => "toggle_selection_properties",
Expand Down
3 changes: 3 additions & 0 deletions configurator/src/models/keybindings/field/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ impl KeybindingField {
Self::ToggleClickHighlight,
Self::ToggleToolbar,
Self::TogglePresenterMode,
Self::RenderProfileNext,
Self::RenderProfilePrevious,
Self::RenderProfileOff,
Self::ToggleFill,
Self::ToggleHighlightTool,
Self::ToggleSelectionProperties,
Expand Down
3 changes: 3 additions & 0 deletions configurator/src/models/keybindings/field/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ pub enum KeybindingField {
ToggleClickHighlight,
ToggleToolbar,
TogglePresenterMode,
RenderProfileNext,
RenderProfilePrevious,
RenderProfileOff,
ToggleFill,
ToggleHighlightTool,
ToggleSelectionProperties,
Expand Down
3 changes: 3 additions & 0 deletions configurator/src/models/keybindings/field/tab.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ impl KeybindingField {
| Self::ToggleClickHighlight
| Self::ToggleToolbar
| Self::TogglePresenterMode
| Self::RenderProfileNext
| Self::RenderProfilePrevious
| Self::RenderProfileOff
| Self::ToggleSelectionProperties
| Self::OpenContextMenu
| Self::ToggleCommandPalette => KeybindingsTabId::UiModes,
Expand Down
45 changes: 45 additions & 0 deletions docs/CONFIG.md
Original file line number Diff line number Diff line change
Expand Up @@ -670,6 +670,46 @@ wayscriber --daemon --mode transparent
This section is still recognized for backward compatibility. If `[boards]` is missing,
wayscriber will synthesize boards from `[board]`. New configurations should prefer `[boards]`.

### `[render_profiles]` - Render Color Profiles

Render profiles preview an alternate final color mapping without changing saved shapes or board
data. They are useful for print, projectors, grayscale-ish previews, and light/dark sharing
workflows.

```toml
[render_profiles]
# Optional profile id to preview on startup
# active = "print"
apply_to_canvas = true
apply_to_ui = true

[[render_profiles.items]]
id = "print"
name = "Print"
mappings = [
{ from = "#000000", to = "#FFFFFF" },
{ from = "#FFFFFF", to = "#000000" },
{ from = "#FFFF00", to = "#8B4513" },
{ from = "#00FF00", to = "#006400" },
]
```

**Behavior:**
- `id` is the stable identifier used by `active` and runtime profile switching.
- `apply_to_canvas` controls board backgrounds, annotations, and canvas-space editor previews such as selections, hover rings, provisional strokes, text-edit previews, and click highlights.
- `apply_to_ui` controls screen-space Wayscriber UI chrome, status text, popups, command palette, and toolbars.
- `mappings` use exact RGB matches. Accepted input forms are `#RRGGBB`, `RRGGBB`, and `0xRRGGBB`; validation normalizes to `#RRGGBB`.
- Pixel alpha is preserved. Unmapped colors are unchanged.
- With both targets enabled, profiles apply to Wayscriber-rendered pixels: annotations, board backgrounds, UI chrome, toolbars, popups, embedded images, and frozen/zoom backgrounds when Wayscriber paints them.
- Set `apply_to_ui = false` to preview remapped canvas content while keeping screen-space UI text and controls in the normal theme.
- Profiles do not recolor the compositor-owned live desktop seen through a transparent overlay.
- Screenshot capture, PDF export, and saved annotation export are not profile-aware in this version.

**Runtime actions:**
- `render_profile_next`
- `render_profile_previous`
- `render_profile_off`

### `[capture]` - Screenshot Capture

Configures how screenshots are stored and shared.
Expand Down Expand Up @@ -909,6 +949,11 @@ toggle_light_mode = ["Ctrl+Shift+L"]
# `wayscriber --light-draw-toggle`, `--light-draw-on`, or `--light-draw-off`.
toggle_light_mode_drawing = []

# Optional render color profile preview controls
render_profile_next = []
render_profile_previous = []
render_profile_off = []

# Toggle click highlight (visual mouse halo)
toggle_click_highlight = ["Ctrl+Shift+H"]

Expand Down
3 changes: 3 additions & 0 deletions src/backend/wayland/backend/state_init/input_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ pub(super) fn build_input_state(config: &Config) -> InputState {
);
input_state.set_action_bindings(action_bindings);
input_state.set_drag_tool_bindings(build_drag_tool_bindings(config));
input_state.set_render_profiles(crate::render_profiles::RenderProfileSet::from_config(
&config.render_profiles,
));

input_state.set_hit_test_tolerance(config.drawing.hit_test_tolerance);
input_state.set_hit_test_threshold(config.drawing.hit_test_linear_threshold);
Expand Down
3 changes: 3 additions & 0 deletions src/backend/wayland/state/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ pub struct StateData {
pub(super) xdg_close_guard_until: Option<Instant>,
/// Explicit compositor close request received for xdg fallback window.
pub(super) xdg_explicit_close_requested: bool,
/// Reused pre-UI pixel snapshot for render-profile UI-only remapping.
pub(super) render_profile_ui_baseline: Vec<u8>,
}

impl StateData {
Expand Down Expand Up @@ -162,6 +164,7 @@ impl StateData {
suppress_focus_exit_until: None,
xdg_close_guard_until: None,
xdg_explicit_close_requested: false,
render_profile_ui_baseline: Vec::new(),
}
}
}
3 changes: 0 additions & 3 deletions src/backend/wayland/state/render/canvas/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ impl WaylandState {
scale: i32,
phys_width: u32,
phys_height: u32,
render_ui: bool,
now: Instant,
damage_world: &[crate::util::Rect],
) -> Result<()> {
Expand Down Expand Up @@ -192,8 +191,6 @@ impl WaylandState {
let _ = ctx.restore();
}

self.render_ui_layers(ctx, width, height, render_ui);

let _ = ctx.restore();

Ok(())
Expand Down
68 changes: 65 additions & 3 deletions src/backend/wayland/state/render/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,12 @@ impl WaylandState {
} else {
damage_screen.clone()
};
let scaled_damage = scale_damage_regions(damage_screen.clone(), scale);
let active_render_profile = self.input_state.active_render_profile().cloned();
let remap_canvas = self.input_state.active_canvas_render_profile().is_some();
let remap_ui = self.input_state.active_ui_render_profile().is_some();
let stride = (phys_width * 4) as i32;
let canvas_len = phys_height as usize * stride as usize;

// SAFETY: This unsafe block creates a Cairo surface from raw memory buffer.
// Safety invariants that must be maintained:
Expand Down Expand Up @@ -184,18 +190,76 @@ impl WaylandState {
scale,
phys_width,
phys_height,
render_ui,
now,
&damage_world,
)?;
}

let mut has_ui_baseline = false;
if let Some(profile) = active_render_profile.as_ref() {
if remap_canvas && !remap_ui {
cairo_surface.flush();
// SAFETY: `canvas_ptr` points to the SlotPool memory for the buffer created above.
// Cairo has flushed all pending writes, so the rendered canvas pixels can be
// rewritten before UI is drawn on top.
let canvas =
unsafe { std::slice::from_raw_parts_mut(canvas_ptr as *mut u8, canvas_len) };
profile.remap_argb8888_regions(
canvas,
phys_width as i32,
phys_height as i32,
stride,
&scaled_damage,
);
cairo_surface.mark_dirty();
} else if !remap_canvas && remap_ui && render_ui {
cairo_surface.flush();
// SAFETY: `canvas_ptr` points to the SlotPool memory for the buffer created above.
// Cairo has flushed all pending writes, so we can snapshot the canvas-only pixels in
// a reusable scratch buffer before drawing UI and later remap only bytes changed by
// the UI pass.
let canvas =
unsafe { std::slice::from_raw_parts_mut(canvas_ptr as *mut u8, canvas_len) };
self.data.render_profile_ui_baseline.resize(canvas_len, 0);
self.data.render_profile_ui_baseline.copy_from_slice(canvas);
has_ui_baseline = true;
}
}

self.render_ui_layer(&ctx, width, height, scale, render_ui);

// Flush Cairo
debug!("Flushing Cairo surface");
cairo_surface.flush();
drop(ctx);
drop(cairo_surface);

if let Some(profile) = active_render_profile.as_ref() {
// SAFETY: `canvas_ptr` points to the SlotPool memory for the buffer created above.
// Cairo has been flushed and dropped, and the buffer has not been attached yet, so this
// is the only active mutable access to the rendered pixel bytes.
let canvas =
unsafe { std::slice::from_raw_parts_mut(canvas_ptr as *mut u8, canvas_len) };
if remap_canvas && remap_ui {
profile.remap_argb8888_regions(
canvas,
phys_width as i32,
phys_height as i32,
stride,
&scaled_damage,
);
} else if !remap_canvas && remap_ui && has_ui_baseline {
profile.remap_argb8888_regions_changed_from(
canvas,
&self.data.render_profile_ui_baseline,
phys_width as i32,
phys_height as i32,
stride,
&scaled_damage,
);
}
}

let draw_duration = draw_start.elapsed();
if draw_duration > std::time::Duration::from_millis(2) {
debug!("Cairo draw took {:?}", draw_duration);
Expand All @@ -214,8 +278,6 @@ impl WaylandState {
// Damage logic moved to top of function (add_regions and take_buffer_damage).
// We now use the computed screen-space damage for clipping and compositor hints.

let scaled_damage = scale_damage_regions(damage_screen.clone(), scale);

if debug_damage_logging_enabled() {
debug!(
"Damage (scaled): count={}, {}",
Expand Down
12 changes: 11 additions & 1 deletion src/backend/wayland/state/render/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,23 @@ use super::tool_preview::{draw_stylus_hover_cursor, draw_tool_preview};
use super::*;

impl WaylandState {
pub(super) fn render_ui_layers(
pub(super) fn render_ui_layer(
&mut self,
ctx: &cairo::Context,
width: u32,
height: u32,
scale: i32,
render_ui: bool,
) {
let _ = ctx.save();
if scale > 1 {
ctx.scale(scale as f64, scale as f64);
}
self.render_ui_layers(ctx, width, height, render_ui);
let _ = ctx.restore();
}

fn render_ui_layers(&mut self, ctx: &cairo::Context, width: u32, height: u32, render_ui: bool) {
if render_ui {
if self.input_state.show_tool_preview
&& self.has_cursor_focus()
Expand Down
4 changes: 3 additions & 1 deletion src/backend/wayland/state/toolbar/visibility/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,9 @@ impl WaylandState {
}

// No hover tracking yet; pass None. Can be updated when we record pointer positions per surface.
self.toolbar.render(&self.shm, snapshot, None);
let render_profile = self.input_state.active_ui_render_profile().cloned();
self.toolbar
.render(&self.shm, snapshot, None, render_profile.as_ref());
}

pub(in crate::backend::wayland) fn render_layer_toolbars_if_needed(&mut self) {
Expand Down
Loading
Loading