Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
ffe1418
fix(patches): implement convert_pointer_data; split events into submo…
claude May 19, 2026
ff053fa
feat(canvas): Phase 3 tool event routing — ToolEvent, dispatch, mouse…
claude May 20, 2026
02da2c1
fix(patches): vendor wgpu_context 0.1.2 to fix Windows/DX12 swap-chai…
claude May 20, 2026
e8c3896
iris-canvas: add STORAGE_BINDING | TEXTURE_BINDING to composite texture
claude May 20, 2026
5a2ec2c
iris-canvas: wrap canvas in div for Blitz mouse event routing
claude May 20, 2026
0ecdf42
iris-tools: BrushEngine + EraserEngine (Phase 3 pixel tool routing)
claude May 20, 2026
2dc44d1
chore: update Cargo.lock for iris-tools crate addition
claude May 20, 2026
c193db6
fix: canvas sizing, coordinate transform, and document boundary rect
claude May 20, 2026
7d5215f
fix: compute canvas rendered size from window minus chrome
claude May 20, 2026
88a19f8
feat: implement onmounted/get_client_rect in dioxus-native-dom patch
claude May 20, 2026
664ab61
fix: correct onmounted timing — fire after first layout, not before
claude May 20, 2026
f125356
Add coordinate pipeline debug logging to diagnose mouse offset
claude May 20, 2026
85d5aeb
Fix canvas size race: source rendered_size from compositor, not onmou…
claude May 20, 2026
88ba6c3
Fix coordinate offset: convert physical to logical pixels in render()
claude May 20, 2026
f73e4e6
Fix coordinate offset: use logical pixel dims for viewport transforms…
claude May 20, 2026
269f094
feat: Phase 3 pixel tools — eyedropper, flood fill, rectangular marquee
claude May 20, 2026
93e7e76
ci: install Wayland and X11 system libraries before cargo test
claude May 20, 2026
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
12 changes: 12 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,18 @@ jobs:

steps:
- uses: actions/checkout@v4
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
libwayland-dev \
libudev-dev \
libxkbcommon-dev \
libx11-dev \
libxi-dev \
libxrandr-dev \
libxcursor-dev \
libgl1-mesa-dev
- name: Build
run: cargo build --verbose
- name: Run tests
Expand Down
22 changes: 16 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 10 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ members = [
"crates/iris-ai",
"crates/iris-svg",
"crates/iris-plugin-api",
"crates/iris-tools",
"tools/aif-inspect",
]

Expand Down Expand Up @@ -42,6 +43,7 @@ iris-vector = { path = "crates/iris-vector" }
iris-aif = { path = "crates/iris-aif" }
iris-canvas = { path = "crates/iris-canvas" }
iris-plugin-api = { path = "crates/iris-plugin-api" }
iris-tools = { path = "crates/iris-tools" }

# External dependencies
dioxus = { version = "=0.7.4", features = ["native"] }
Expand All @@ -66,10 +68,9 @@ bytemuck = { version = "1", features = ["derive"] }
png = { version = "0.17" }

[patch.crates-io]
# PATCH: fixes missing fontconfig_sys alias and dlopen/static feature-unification
# conflict with blitz-dom's fontique 0.6 — remove when fontique >0.8.0 on
# crates.io restores the alias and resolves the linkage conflict.
fontique = { path = "crates/patches/fontique" }
# COMPAT(dioxus): fontique patch removed — blitz-dom 0.2.4 resolves fontique 0.6.0
# from crates.io directly; the 0.8.0-based patch was version-mismatched and never
# applied. Re-evaluate if fontconfig_sys linkage errors appear on CI Linux.
# PATCH: vendors dioxus-native-dom to avoid runtime unimplemented!() panics in
# HtmlEventConverter (composition, touch, scroll, etc.) — remove when upstream
# implements the converters Loki requires.
Expand All @@ -81,6 +82,11 @@ blitz-dom = { path = "crates/patches/blitz-dom" }
# PATCH: forwards WindowEvent::Touch to Dioxus touch handlers.
# Remove when blitz-shell implements touch natively.
blitz-shell = { path = "crates/patches/blitz-shell" }
# PATCH: handle SurfaceError::Outdated/Lost gracefully on Windows/DX12.
# Without this, registering a CustomPaintSource via use_wgpu panics on the
# first render cycle when the swap chain is not yet ready. Remove when
# wgpu_context upstream fixes the TODO at surface_renderer.rs:228.
wgpu_context = { path = "crates/patches/wgpu-context" }

[profile.dev]
opt-level = 1 # faster compiles, still debuggable
Expand Down
2 changes: 2 additions & 0 deletions crates/iris-app/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ path = "src/main.rs"
appthere-ui = { workspace = true }
iris-canvas = { workspace = true }
iris-pixel = { workspace = true }
iris-tools = { workspace = true }
kurbo = { workspace = true }
dioxus = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
Expand Down
51 changes: 45 additions & 6 deletions crates/iris-app/src/canvas_area.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,29 @@
// SPDX-License-Identifier: Apache-2.0

use appthere_ui::tokens::colors::{COLOR_SURFACE_BASE, COLOR_TEXT_ON_CHROME_SECONDARY};
use appthere_ui::tokens::layout::{
RIBBON_TOTAL_HEIGHT, STATUS_BAR_HEIGHT, TAB_BAR_HEIGHT, TITLE_BAR_HEIGHT_MACOS,
};
use appthere_ui::tokens::typography::FONT_SIZE_BODY;
use dioxus::prelude::*;
use iris_canvas::IrisCanvas;
use iris_pixel::LayerTree;

use crate::state::AppState;

// TODO(iris): SPEC.md §11.3 — measure actual CanvasArea dimensions via onmounted
const CANVAS_WIDTH: u32 = 800;
const CANVAS_HEIGHT: u32 = 600;
// Chrome dimensions used to derive the canvas rendered area from window size.
// Left/right panel widths are defined in tool_palette.rs / layers_panel.rs;
// duplicated here until they are promoted to appthere-ui layout tokens.
// TODO(iris): SPEC.md §11.3 — move to appthere-ui layout tokens
const CHROME_LEFT: u32 = 56; // TOOL_PALETTE_WIDTH
const CHROME_RIGHT: u32 = 240; // LAYERS_PANEL_WIDTH
const CHROME_TOP: u32 = (TITLE_BAR_HEIGHT_MACOS + TAB_BAR_HEIGHT + RIBBON_TOTAL_HEIGHT) as u32;
const CHROME_BOTTOM: u32 = STATUS_BAR_HEIGHT as u32;

#[component]
pub fn CanvasArea(mut state: Signal<AppState>) -> Element {
// All hooks called unconditionally before any early return (Dioxus rules).
let tree_signal = use_signal(move || {
let mut tree_signal = use_signal(move || {
state
.read()
.document
Expand All @@ -41,6 +49,34 @@ pub fn CanvasArea(mut state: Signal<AppState>) -> Element {
}
});

// Sync tree_signal → IrisCanvas when a stroke has painted new tile data.
// canvas_dirty is set by tool_dispatch after each Down/Move/Up event.
use_effect(move || {
if state.read().canvas_dirty {
let new_tree = state
.read()
.document
.as_ref()
.map(|d| d.tree.clone())
.unwrap_or_else(|| LayerTree::new(1, 1, 96.0, 96.0));
*tree_signal.write() = new_tree;
state.write().canvas_dirty = false;
}
});

// Compute canvas rendered size = window size minus shell chrome.
// TODO(iris): SPEC.md §11.3 — replace hardcoded window size with a real
// window-size hook once dioxus-native exposes one (winit PhysicalSize is
// available but no Dioxus hook wraps it yet).
let window_w = use_signal(|| 1280u32);
let window_h = use_signal(|| 800u32);
let canvas_w = use_memo(move || {
window_w().saturating_sub(CHROME_LEFT + CHROME_RIGHT).max(100)
});
let canvas_h = use_memo(move || {
window_h().saturating_sub(CHROME_TOP + CHROME_BOTTOM).max(100)
});

let doc_exists = state.read().document.is_some();

// Early return AFTER all hooks.
Expand All @@ -62,8 +98,11 @@ pub fn CanvasArea(mut state: Signal<AppState>) -> Element {
IrisCanvas {
tree: tree_signal,
viewport: viewport_signal,
width: CANVAS_WIDTH,
height: CANVAS_HEIGHT,
width: canvas_w(),
height: canvas_h(),
on_tool_event: move |evt| {
crate::tool_dispatch::dispatch_tool_event(evt, state);
},
}
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/iris-app/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ mod layout;
mod ribbon;
mod state;
mod status_bar;
mod tool_dispatch;
mod tool_palette;

use app::App;
Expand Down
68 changes: 59 additions & 9 deletions crates/iris-app/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,57 @@ use iris_pixel::{
BitDepth, BlendMode, ChannelLayout, ExrCompression, Layer, LayerContent, LayerId, LayerTree,
PixelLayer, TileCache, LINEAR_SRGB,
};
use iris_tools::{BrushEngine, BrushSettings, EraserEngine};

/// Which pixel sub-tool is active within [`ToolMode::Pixel`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PixelTool {
#[default]
Brush,
Eraser,
Eyedropper,
Fill,
Marquee,
}

/// Active marquee selection in document space. `None` = no selection (paint everywhere).
#[derive(Debug, Clone, Default)]
pub struct Selection {
pub rect: Option<kurbo::Rect>,
}

/// Active pixel-tool state, stored in AppState so dispatch_tool_event can mutate it.
#[derive(Clone)]
pub struct PixelToolState {
pub brush: BrushEngine,
pub eraser: EraserEngine,
/// Flood-fill colour-similarity threshold (0.0–1.0 Euclidean RGB distance).
pub fill_tolerance: f32,
}

impl Default for PixelToolState {
fn default() -> Self {
Self { brush: BrushEngine::default(), eraser: EraserEngine::default(), fill_tolerance: 0.1 }
}
}

#[derive(Clone)]
pub struct AppState {
pub document: Option<OpenDocument>,
pub tool_mode: ToolMode,
pub active_pixel_tool: PixelTool,
pub selected_layer: Option<LayerId>,
pub pixel_tool_state: PixelToolState,
/// Foreground (paint) colour in linear RGBA. Default: opaque black.
pub foreground_color: [f32; 4],
/// Background colour in linear RGBA. Default: opaque white.
pub background_color: [f32; 4],
/// Current marquee selection (document pixels). Updated by the Marquee tool.
pub selection: Selection,
/// Drag origin for the marquee tool; cleared on ToolEvent::Up.
pub marquee_start: Option<kurbo::Vec2>,
/// Set to `true` when a stroke dirties tiles; canvas_area.rs syncs tree_signal.
pub canvas_dirty: bool,
pub active_tab_index: usize,
pub platform: Platform,
}
Expand All @@ -36,10 +81,19 @@ pub enum ToolMode {

impl Default for AppState {
fn default() -> Self {
let doc = OpenDocument::new_blank(800, 600, "Untitled");
let selected_layer = doc.tree.root_layer_ids().first().copied();
Self {
document: Some(OpenDocument::new_blank(800, 600, "Untitled")),
document: Some(doc),
tool_mode: ToolMode::Pixel,
selected_layer: None,
active_pixel_tool: PixelTool::Brush,
selected_layer,
pixel_tool_state: PixelToolState::default(),
foreground_color: [0.0, 0.0, 0.0, 1.0],
background_color: [1.0, 1.0, 1.0, 1.0],
selection: Selection::default(),
marquee_start: None,
canvas_dirty: false,
active_tab_index: 1,
platform: detect_platform(),
}
Expand Down Expand Up @@ -71,13 +125,9 @@ impl OpenDocument {
};
tree.add_layer(None, 0, layer)
.expect("new_blank: adding initial layer to empty tree cannot fail");
Self {
title: title.to_string(),
path: None,
tree,
viewport: CanvasViewport::new(),
dirty: false,
}
let mut viewport = CanvasViewport::new();
viewport.pan = kurbo::Vec2::new(width_px as f64 / 2.0, height_px as f64 / 2.0);
Self { title: title.to_string(), path: None, tree, viewport, dirty: false }
}

pub fn zoom_percent(&self) -> u32 {
Expand Down
Loading
Loading