use crate::compat::Arc;
use crate::core::{Color, Point, Rect};
use crate::event::{Event, EventHandler};
use crate::render::{PaintBackend, RenderCommand, RenderContext};
use crate::signal::GenericSignal;
use crate::signal::Signal1;
use crate::widget::capability::properties_trait::{base_property_get, base_property_set};
use crate::widget::capability::types::{CapabilityAccessError, CapabilityValue};
use crate::widget::capability::WidgetProperties;
use crate::widget::{BaseWidget, Draw, Widget, WidgetKind};
use crate::{impl_widget_property_hooks, property_names_of};
use alloc::collections::VecDeque;
use core::ops::Deref;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CanvasTransform {
pub scale: f32,
pub offset: Point,
}
impl Default for CanvasTransform {
fn default() -> Self {
Self { scale: 1.0, offset: Point::new(0, 0) }
}
}
impl CanvasTransform {
pub fn scaled(scale: f32) -> Self {
Self { scale: if scale.is_finite() { scale.max(0.01) } else { 1.0 }, ..Self::default() }
}
pub fn point(&self, point: Point) -> Point {
Point::new(
self.offset.x + (point.x as f32 * self.scale).round() as i32,
self.offset.y + (point.y as f32 * self.scale).round() as i32,
)
}
pub fn rect(&self, rect: Rect) -> Rect {
let origin = self.point(Point::new(rect.x, rect.y));
Rect::new(
origin.x,
origin.y,
(rect.width as f32 * self.scale).round().max(1.0) as u32,
(rect.height as f32 * self.scale).round().max(1.0) as u32,
)
}
pub fn inverse_point(&self, point: Point) -> Point {
let scale = if self.scale.is_finite() && self.scale > 0.0 { self.scale } else { 1.0 };
Point::new(
((point.x - self.offset.x) as f32 / scale).round() as i32,
((point.y - self.offset.y) as f32 / scale).round() as i32,
)
}
}
#[derive(Clone, Default)]
pub struct CanvasPointerEvent {
unit: GenericSignal,
payload: Signal1<(Point, u32)>,
}
impl CanvasPointerEvent {
pub fn new() -> Self {
Self { unit: GenericSignal::new(), payload: Signal1::new() }
}
pub fn payload_signal(&self) -> &Signal1<(Point, u32)> {
&self.payload
}
pub fn emit_at(&self, position: Point, button: u32) {
self.unit.emit();
self.payload.emit((position, button));
}
}
impl Deref for CanvasPointerEvent {
type Target = GenericSignal;
fn deref(&self) -> &Self::Target {
&self.unit
}
}
struct StoredCommand {
seq: u64,
command: RenderCommand,
replay: Option<(CanvasTransform, RenderCommand)>,
}
impl StoredCommand {
fn replay_under(&mut self, transform: CanvasTransform) -> &RenderCommand {
let fresh = matches!(&self.replay, Some((cached, _)) if *cached == transform);
if !fresh {
let mapped = transform_command(&self.command, transform);
self.replay = Some((transform, mapped));
}
match self.replay.as_ref() {
Some((_, command)) => command,
None => &self.command,
}
}
}
fn next_layer_cache_key() -> u64 {
use core::sync::atomic::{AtomicU64, Ordering};
static NEXT: AtomicU64 = AtomicU64::new(1);
NEXT.fetch_add(1, Ordering::Relaxed)
}
struct CanvasLayer {
id: u64,
name: String,
visible: bool,
commands: VecDeque<StoredCommand>,
cache_key: u64,
}
struct UndoneCommand {
layer_id: u64,
command: RenderCommand,
}
#[derive(Clone, Debug)]
pub struct CanvasImage {
pub data: Arc<[u8]>,
pub width: u32,
pub height: u32,
}
impl CanvasImage {
pub fn new(width: u32, height: u32, pixels: Vec<u8>) -> Option<Self> {
let expected = width as usize * height as usize * 4;
(width > 0 && height > 0 && pixels.len() >= expected).then(|| Self {
data: Arc::from(pixels),
width,
height,
})
}
}
pub struct Canvas {
base: BaseWidget,
layers: Vec<CanvasLayer>,
current_layer: Option<u64>,
next_layer_id: u64,
next_command_seq: u64,
transform: CanvasTransform,
transform_stack: Vec<CanvasTransform>,
undo_stack: VecDeque<Vec<(u64, u64)>>,
redo_stack: VecDeque<Vec<UndoneCommand>>,
pending_group: Option<Vec<(u64, u64)>>,
command_limit: usize,
command_total: usize,
grid_cache_key: u64,
zoom_enabled: bool,
anti_alias: bool,
show_grid: bool,
grid_step: u32,
snap_to_grid: bool,
zoom_min: f32,
zoom_max: f32,
zoom_step: f32,
pub context_menu_requested: Signal1<Point>,
last_mouse_pos: Point,
pub mouse_pressed: CanvasPointerEvent,
pub mouse_released: CanvasPointerEvent,
pub mouse_moved: crate::signal::Signal1<Point>,
pub double_clicked: CanvasPointerEvent,
pub layer_changed: Signal1<usize>,
pub content_changed: Signal1<usize>,
pub zoom_changed: Signal1<f32>,
pub command_picked: Signal1<usize>,
}
impl Canvas {
pub fn new(geometry: Rect) -> Self {
Self {
base: BaseWidget::new(WidgetKind::Canvas, geometry, "Canvas"),
layers: Vec::new(),
current_layer: None,
next_layer_id: 1,
next_command_seq: 1,
transform: CanvasTransform::default(),
transform_stack: Vec::new(),
undo_stack: VecDeque::new(),
redo_stack: VecDeque::new(),
pending_group: None,
command_limit: 100_000,
command_total: 0,
grid_cache_key: next_layer_cache_key(),
zoom_enabled: true,
anti_alias: true,
show_grid: false,
grid_step: 20,
snap_to_grid: false,
zoom_min: 0.1,
zoom_max: 16.0,
zoom_step: 1.1,
context_menu_requested: Signal1::new(),
last_mouse_pos: Point::new(0, 0),
mouse_pressed: CanvasPointerEvent::new(),
mouse_released: CanvasPointerEvent::new(),
mouse_moved: crate::signal::Signal1::new(),
double_clicked: CanvasPointerEvent::new(),
layer_changed: Signal1::new(),
content_changed: Signal1::new(),
zoom_changed: Signal1::new(),
command_picked: Signal1::new(),
}
}
pub fn mouse_pressed_signal(&self) -> &Signal1<(Point, u32)> {
self.mouse_pressed.payload_signal()
}
pub fn mouse_released_signal(&self) -> &Signal1<(Point, u32)> {
self.mouse_released.payload_signal()
}
pub fn double_clicked_signal(&self) -> &Signal1<(Point, u32)> {
self.double_clicked.payload_signal()
}
pub fn clear(&mut self) {
self.layers.clear();
self.command_total = 0;
self.current_layer = None;
self.undo_stack.clear();
self.redo_stack.clear();
self.pending_group = None;
self.content_changed.emit(0);
self.layer_changed.emit(0);
self.base.request_redraw();
}
pub fn command_count(&self) -> usize {
self.command_total
}
pub fn layer_count(&self) -> usize {
self.layers.len()
}
pub fn begin_layer(&mut self, name: impl Into<String>) -> usize {
let name = name.into();
if let Some(index) = self.layers.iter().position(|layer| layer.name == name) {
self.current_layer = Some(self.layers[index].id);
return index;
}
let id = self.next_layer_id;
self.next_layer_id += 1;
self.layers.push(CanvasLayer {
id,
name,
visible: true,
commands: VecDeque::new(),
cache_key: next_layer_cache_key(),
});
self.current_layer = Some(id);
self.layer_changed.emit(self.layers.len());
self.base.request_redraw();
self.layers.len() - 1
}
pub fn set_layer_visible(&mut self, index: usize, visible: bool) -> bool {
let Some(layer) = self.layers.get_mut(index) else { return false };
if layer.visible == visible {
return true;
}
layer.visible = visible;
self.layer_changed.emit(self.layers.len());
self.base.request_redraw();
true
}
pub fn is_layer_visible(&self, index: usize) -> bool {
self.layers.get(index).map(|layer| layer.visible).unwrap_or(false)
}
pub fn move_layer(&mut self, from: usize, to: usize) -> bool {
if from >= self.layers.len() || to > self.layers.len() {
return false;
}
if from == to {
return true;
}
let layer = self.layers.remove(from);
self.layers.insert(to.min(self.layers.len()), layer);
self.layer_changed.emit(self.layers.len());
self.base.request_redraw();
true
}
pub fn remove_layer(&mut self, index: usize) -> bool {
if index >= self.layers.len() {
return false;
}
let removed_len = self.layers[index].commands.len();
let layer = self.layers.remove(index);
self.command_total = self.command_total.saturating_sub(removed_len);
for step in &mut self.undo_stack {
step.retain(|(layer_id, _)| *layer_id != layer.id);
}
self.undo_stack.retain(|step| !step.is_empty());
self.redo_stack.retain_mut(|step| {
step.retain(|undone| undone.layer_id != layer.id);
!step.is_empty()
});
if self.current_layer == Some(layer.id) {
self.current_layer = None;
}
let count = self.command_count();
self.layer_changed.emit(self.layers.len());
self.content_changed.emit(count);
self.base.request_redraw();
true
}
pub fn transform(&self) -> CanvasTransform {
self.transform
}
pub fn set_transform(&mut self, transform: CanvasTransform) {
if self.transform == transform {
return;
}
let scale_changed = self.transform.scale != transform.scale;
self.transform = transform;
self.invalidate_replay_caches();
if scale_changed {
self.zoom_changed.emit(self.transform.scale);
}
self.base.request_redraw();
}
fn invalidate_replay_caches(&mut self) {
for layer in &mut self.layers {
for stored in &mut layer.commands {
stored.replay = None;
}
layer.cache_key = next_layer_cache_key();
}
self.grid_cache_key = next_layer_cache_key();
}
pub fn push_transform(&mut self) {
self.transform_stack.push(self.transform);
}
pub fn pop_transform(&mut self) {
if let Some(transform) = self.transform_stack.pop() {
if self.transform != transform {
self.transform = transform;
self.invalidate_replay_caches();
self.zoom_changed.emit(transform.scale);
}
self.base.request_redraw();
}
}
pub fn set_zoom_enabled(&mut self, enabled: bool) {
if self.zoom_enabled == enabled {
return;
}
self.zoom_enabled = enabled;
}
pub fn is_zoom_enabled(&self) -> bool {
self.zoom_enabled
}
pub fn anti_alias(&self) -> bool {
self.anti_alias
}
pub fn set_anti_alias(&mut self, enabled: bool) {
if self.anti_alias == enabled {
return;
}
self.anti_alias = enabled;
if !enabled {
for layer in &mut self.layers {
let commands = core::mem::take(&mut layer.commands);
layer.commands = commands
.into_iter()
.map(|mut stored| {
stored.command = without_anti_aliasing(stored.command);
stored.replay = None;
stored
})
.collect();
layer.cache_key = next_layer_cache_key();
}
}
self.base.request_redraw();
}
pub fn show_grid(&self) -> bool {
self.show_grid
}
pub fn set_show_grid(&mut self, show: bool) {
if self.show_grid == show {
return;
}
self.show_grid = show;
self.grid_cache_key = next_layer_cache_key();
self.base.request_redraw();
}
pub fn grid_step(&self) -> u32 {
self.grid_step
}
pub fn set_grid_step(&mut self, step: u32) {
let step = step.max(1);
if self.grid_step == step {
return;
}
self.grid_step = step;
self.grid_cache_key = next_layer_cache_key();
self.base.request_redraw();
}
pub fn snap_to_grid(&self) -> bool {
self.snap_to_grid
}
pub fn set_snap_to_grid(&mut self, snap: bool) {
if self.snap_to_grid == snap {
return;
}
self.snap_to_grid = snap;
}
pub fn zoom_range(&self) -> (f32, f32) {
(self.zoom_min, self.zoom_max)
}
pub fn set_zoom_range(&mut self, min: f32, max: f32) -> bool {
if !min.is_finite() || !max.is_finite() || min <= 0.0 || min >= max {
return false;
}
self.zoom_min = min;
self.zoom_max = max;
let scale = self.transform.scale.clamp(min, max);
if scale != self.transform.scale {
self.transform.scale = scale;
self.invalidate_replay_caches();
self.zoom_changed.emit(scale);
self.base.request_redraw();
}
true
}
pub fn zoom_step(&self) -> f32 {
self.zoom_step
}
pub fn set_zoom_step(&mut self, step: f32) {
if !step.is_finite() {
return;
}
self.zoom_step = step.clamp(1.01, 10.0);
}
pub fn set_command_limit(&mut self, limit: usize) {
self.command_limit = limit.max(1);
self.trim_to_limit();
while self.undo_stack.len() > self.command_limit {
self.undo_stack.pop_front();
}
while self.redo_stack.len() > self.command_limit {
self.redo_stack.pop_front();
}
}
pub fn command_limit(&self) -> usize {
self.command_limit
}
pub fn begin_group(&mut self) -> bool {
if self.pending_group.is_some() {
return false;
}
self.pending_group = Some(Vec::new());
true
}
pub fn end_group(&mut self) -> bool {
let Some(group) = self.pending_group.take() else { return false };
if group.is_empty() {
return false;
}
self.undo_stack.push_back(group);
self.trim_undo_stack();
true
}
pub fn undo(&mut self) -> bool {
loop {
let Some(step) = self.undo_stack.pop_back() else { return false };
let mut removed = Vec::new();
for (layer_id, seq) in step.into_iter().rev() {
if let Some(command) = self.take_command(layer_id, seq) {
removed.push(UndoneCommand { layer_id, command });
}
}
if removed.is_empty() {
continue;
}
removed.reverse();
self.redo_stack.push_back(removed);
self.trim_redo_stack();
self.content_changed.emit(self.command_count());
self.base.request_redraw();
return true;
}
}
pub fn redo(&mut self) -> bool {
loop {
let Some(step) = self.redo_stack.pop_back() else { return false };
let mut restored = Vec::new();
for undone in step {
let Some(layer_index) = self.layer_index_by_id(undone.layer_id) else { continue };
let seq = self.next_command_seq;
self.next_command_seq += 1;
self.layers[layer_index].commands.push_back(StoredCommand {
seq,
command: undone.command,
replay: None,
});
self.layers[layer_index].cache_key = next_layer_cache_key();
self.command_total = self.command_total.saturating_add(1);
restored.push((undone.layer_id, seq));
}
if restored.is_empty() {
continue;
}
self.undo_stack.push_back(restored);
self.trim_undo_stack();
self.trim_to_limit();
self.content_changed.emit(self.command_count());
self.base.request_redraw();
return true;
}
}
pub fn can_redo(&self) -> bool {
!self.redo_stack.is_empty()
}
pub fn undo_count(&self) -> usize {
self.undo_stack.len()
}
pub fn redo_count(&self) -> usize {
self.redo_stack.len()
}
pub fn can_undo(&self) -> bool {
!self.undo_stack.is_empty()
}
fn layer_index_by_id(&self, id: u64) -> Option<usize> {
self.layers.iter().position(|layer| layer.id == id)
}
fn ensure_layer(&mut self) -> usize {
if let Some(index) = self.current_layer.and_then(|id| self.layer_index_by_id(id)) {
return index;
}
let id = self.next_layer_id;
self.next_layer_id += 1;
self.layers.push(CanvasLayer {
id,
name: String::from("default"),
visible: true,
commands: VecDeque::new(),
cache_key: next_layer_cache_key(),
});
self.current_layer = Some(id);
self.layer_changed.emit(self.layers.len());
self.layers.len() - 1
}
fn take_command(&mut self, layer_id: u64, seq: u64) -> Option<RenderCommand> {
let layer_index = self.layer_index_by_id(layer_id)?;
let commands = &mut self.layers[layer_index].commands;
let index = commands.iter().position(|stored| stored.seq == seq)?;
let removed = commands.remove(index).map(|stored| stored.command);
if removed.is_some() {
self.command_total = self.command_total.saturating_sub(1);
self.layers[layer_index].cache_key = next_layer_cache_key();
}
removed
}
fn oldest_command_layer(&self) -> Option<usize> {
let mut oldest: Option<(usize, u64)> = None;
for (layer_index, layer) in self.layers.iter().enumerate() {
if let Some(first) = layer.commands.front() {
if oldest.is_none_or(|(_, seq)| first.seq < seq) {
oldest = Some((layer_index, first.seq));
}
}
}
oldest.map(|(layer_index, _)| layer_index)
}
fn trim_to_limit(&mut self) {
while self.command_count() > self.command_limit {
let Some(layer_index) = self.oldest_command_layer() else { break };
let layer_id = self.layers[layer_index].id;
let Some(removed) = self.layers[layer_index].commands.pop_front() else { break };
self.command_total = self.command_total.saturating_sub(1);
let seq = removed.seq;
for step in &mut self.undo_stack {
step.retain(|(id, entry_seq)| !(*id == layer_id && *entry_seq == seq));
}
self.undo_stack.retain(|step| !step.is_empty());
if let Some(group) = &mut self.pending_group {
group.retain(|(id, entry_seq)| !(*id == layer_id && *entry_seq == seq));
}
}
}
fn trim_undo_stack(&mut self) {
while self.undo_stack.len() > self.command_limit {
self.undo_stack.pop_front();
}
}
fn trim_redo_stack(&mut self) {
while self.redo_stack.len() > self.command_limit {
self.redo_stack.pop_front();
}
}
fn push_command(&mut self, command: RenderCommand) {
let mut command = command;
if self.snap_to_grid {
command = snap_command(command, self.grid_step.max(1) as i32);
}
if !self.anti_alias {
command = without_anti_aliasing(command);
}
let layer_index = self.ensure_layer();
let layer_id = self.layers[layer_index].id;
let seq = self.next_command_seq;
self.next_command_seq += 1;
self.layers[layer_index].commands.push_back(StoredCommand { seq, command, replay: None });
self.layers[layer_index].cache_key = next_layer_cache_key();
self.command_total = self.command_total.saturating_add(1);
self.redo_stack.clear();
let reference = (layer_id, seq);
if let Some(group) = &mut self.pending_group {
group.push(reference);
} else {
self.undo_stack.push_back(vec![reference]);
self.trim_undo_stack();
}
self.trim_to_limit();
self.content_changed.emit(self.command_count());
self.base.request_redraw();
}
pub fn export_png(&self, width: u32, height: u32, scale: f32) -> Result<Vec<u8>, String> {
if width == 0 || height == 0 {
return Err("cannot export a zero-sized canvas".into());
}
let scale = if scale.is_finite() && scale > 0.0 { scale } else { 1.0 };
let size = crate::core::Size::new(width, height);
let mut backend = crate::render::SoftwarePaintBackend::new(size, scale);
backend.begin_frame(Color::TRANSPARENT);
{
let mut context = RenderContext::new(&mut backend);
for layer in &self.layers {
if !layer.visible {
continue;
}
for stored in &layer.commands {
let mapped = transform_command(&stored.command, self.transform);
context.execute_command(mapped);
}
}
}
backend.end_frame();
let rgba = backend.surface().frame_rgba().to_vec();
let image = crate::image::DecodedImage::new(
crate::image::ImageFormat::Png,
crate::image::ImageData::Rgba8(rgba),
width,
height,
);
crate::image::encode(&image, crate::image::ImageFormat::Png)
}
pub fn hit_test(&self, point: Point) -> Option<usize> {
let local = self.transform.inverse_point(point);
let mut end = self.command_total;
for layer in self.layers.iter().rev() {
let start = end.saturating_sub(layer.commands.len());
if layer.visible {
for (command_index, stored) in layer.commands.iter().enumerate().rev() {
if let Some(bounds) = command_bounds(&stored.command) {
if bounds.contains_point(local) {
return Some(start + command_index);
}
}
}
}
end = start;
}
None
}
pub fn export_svg(&mut self) -> String {
crate::widget::svg::render_to_svg(self)
}
pub fn fill_rect(&mut self, rect: Rect, color: Color) {
self.push_command(RenderCommand::FillRect { rect, color });
self.base.request_redraw();
}
pub fn draw_rect(&mut self, rect: Rect, color: Color) {
self.push_command(RenderCommand::DrawRect { rect, color });
self.base.request_redraw();
}
pub fn draw_line(&mut self, from: Point, to: Point, color: Color) {
self.push_command(RenderCommand::DrawLine { from, to, color });
self.base.request_redraw();
}
pub fn draw_line_aa(&mut self, from: Point, to: Point, color: Color) {
self.push_command(RenderCommand::DrawLineAA { from, to, color });
self.base.request_redraw();
}
pub fn draw_line_stroke(&mut self, from: Point, to: Point, color: Color, width: u32) {
self.push_command(RenderCommand::DrawLineStroke { from, to, color, width });
self.base.request_redraw();
}
pub fn fill_circle(&mut self, center: Point, radius: u32, color: Color) {
self.push_command(RenderCommand::FillCircle { center, radius, color });
self.base.request_redraw();
}
pub fn fill_circle_aa(&mut self, center: Point, radius: u32, color: Color) {
self.push_command(RenderCommand::FillCircleAA { center, radius, color });
self.base.request_redraw();
}
pub fn draw_circle(&mut self, center: Point, radius: u32, color: Color) {
self.push_command(RenderCommand::DrawCircle { center, radius, color });
self.base.request_redraw();
}
pub fn draw_circle_stroke(&mut self, center: Point, radius: u32, color: Color, width: u32) {
self.push_command(RenderCommand::DrawCircleStroke { center, radius, color, width });
self.base.request_redraw();
}
pub fn fill_rounded_rect(&mut self, rect: Rect, radius: u32, color: Color) {
self.push_command(RenderCommand::FillRoundedRect { rect, radius, color });
self.base.request_redraw();
}
pub fn draw_text(&mut self, origin: Point, text: &str, font: &crate::core::Font, color: Color) {
self.push_command(RenderCommand::DrawText {
origin,
text: Arc::from(text),
font: font.clone(),
color,
alignment: crate::core::HorizontalAlignment::Left,
});
self.base.request_redraw();
}
pub fn draw_arc(
&mut self,
center: Point,
radius: u32,
start_angle: f32,
end_angle: f32,
color: Color,
filled: bool,
) {
self.push_command(RenderCommand::DrawArc {
center,
radius,
start_angle,
end_angle,
color,
filled,
});
self.base.request_redraw();
}
pub fn draw_path(
&mut self,
points: Vec<Point>,
closed: bool,
color: Color,
filled: bool,
width: u32,
) {
self.push_command(RenderCommand::DrawPath {
points: points.into(),
closed,
color,
filled,
width,
});
self.base.request_redraw();
}
pub fn draw_image(&mut self, image: CanvasImage, rect: Rect) {
let rect = if rect.width == 0 || rect.height == 0 {
Rect::new(rect.x, rect.y, image.width, image.height)
} else {
rect
};
self.push_command(RenderCommand::DrawImage {
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
data: image.data,
});
self.base.request_redraw();
}
pub fn last_mouse_pos(&self) -> Point {
self.last_mouse_pos
}
}
impl Widget for Canvas {
fn base(&self) -> &BaseWidget {
&self.base
}
fn set_state_theme_hook(&mut self) {
crate::style::reapply_active_theme_state(self);
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
fn size_hint(&self) -> crate::core::Size {
crate::core::Size::new(300, 200)
}
impl_draw_bridge!();
impl_widget_property_hooks!();
fn event_signal_dyn(&self, name: &str) -> Option<crate::signal::EventSignalRef> {
use crate::signal::EventSignalRef;
match name {
"mouse_pressed" => Some(EventSignalRef::mapped(
"mouse_pressed",
self.mouse_pressed_signal(),
|value| {
let (position, button) = value;
CapabilityValue::Tuple(crate::compat::Vec::from([
CapabilityValue::String(format!("{},{}", position.x, position.y)),
CapabilityValue::UInt(*button as u64),
]))
},
)),
"mouse_released" => Some(EventSignalRef::mapped(
"mouse_released",
self.mouse_released_signal(),
|value| {
let (position, button) = value;
CapabilityValue::Tuple(crate::compat::Vec::from([
CapabilityValue::String(format!("{},{}", position.x, position.y)),
CapabilityValue::UInt(*button as u64),
]))
},
)),
"mouse_moved" => Some(EventSignalRef::mapped("mouse_moved", &self.mouse_moved, |v| {
CapabilityValue::String(format!("{v:?}"))
})),
"double_clicked" => Some(EventSignalRef::mapped(
"double_clicked",
self.double_clicked_signal(),
|value| {
let (position, button) = value;
CapabilityValue::Tuple(crate::compat::Vec::from([
CapabilityValue::String(format!("{},{}", position.x, position.y)),
CapabilityValue::UInt(*button as u64),
]))
},
)),
"context_menu_requested" => Some(EventSignalRef::mapped(
"context_menu_requested",
&self.context_menu_requested,
|v| crate::widget::capability::CapabilityValue::String(format!("{},{}", v.x, v.y)),
)),
"layer_changed" => {
Some(EventSignalRef::mapped("layer_changed", &self.layer_changed, |v| {
CapabilityValue::UInt(*v as u64)
}))
}
"content_changed" => {
Some(EventSignalRef::mapped("content_changed", &self.content_changed, |v| {
CapabilityValue::UInt(*v as u64)
}))
}
"zoom_changed" => {
Some(EventSignalRef::mapped("zoom_changed", &self.zoom_changed, |v| {
CapabilityValue::Float(*v as f64)
}))
}
"command_picked" => {
Some(EventSignalRef::mapped("command_picked", &self.command_picked, |v| {
CapabilityValue::UInt(*v as u64)
}))
}
_ => None,
}
}
}
impl WidgetProperties for Canvas {
fn get(&self, name: &str) -> Result<CapabilityValue, CapabilityAccessError> {
match name {
"command_count" => Ok(CapabilityValue::UInt(self.command_count() as u64)),
"layer_count" => Ok(CapabilityValue::UInt(self.layer_count() as u64)),
"anti_alias" => Ok(CapabilityValue::Bool(self.anti_alias())),
"show_grid" => Ok(CapabilityValue::Bool(self.show_grid())),
"grid_step" => Ok(CapabilityValue::UInt(self.grid_step() as u64)),
"snap_to_grid" => Ok(CapabilityValue::Bool(self.snap_to_grid())),
"zoom_range" => {
let (min, max) = self.zoom_range();
Ok(CapabilityValue::Tuple(crate::compat::Vec::from([
CapabilityValue::Float(min as f64),
CapabilityValue::Float(max as f64),
])))
}
"zoom_step" => Ok(CapabilityValue::Float(self.zoom_step() as f64)),
_ => base_property_get(self, name),
}
}
fn set(&mut self, name: &str, value: CapabilityValue) -> Result<(), CapabilityAccessError> {
match name {
"command_count" | "layer_count" => Err(CapabilityAccessError::ReadOnlyProperty),
"anti_alias" => {
self.set_anti_alias(crate::widget::capability::coercion::expect_bool(value)?);
Ok(())
}
"show_grid" => {
self.set_show_grid(crate::widget::capability::coercion::expect_bool(value)?);
Ok(())
}
"grid_step" => {
let step = crate::widget::capability::coercion::expect_u32(value)?;
self.set_grid_step(step);
Ok(())
}
"snap_to_grid" => {
self.set_snap_to_grid(crate::widget::capability::coercion::expect_bool(value)?);
Ok(())
}
"zoom_range" => {
let bounds = crate::widget::capability::coercion::expect_number_list(value)?;
if bounds.len() != 2 {
return Err(CapabilityAccessError::TypeMismatch);
}
if self.set_zoom_range(bounds[0] as f32, bounds[1] as f32) {
Ok(())
} else {
Err(CapabilityAccessError::OutOfRange)
}
}
"zoom_step" => {
let step = crate::widget::capability::coercion::expect_f32(value)?;
if !step.is_finite() || step <= 1.0 {
return Err(CapabilityAccessError::OutOfRange);
}
self.set_zoom_step(step);
Ok(())
}
_ => base_property_set(self, name, value),
}
}
fn property_names(&self) -> &'static [&'static str] {
property_names_of![
"command_count",
"layer_count",
"anti_alias",
"show_grid",
"grid_step",
"snap_to_grid",
"zoom_range",
"zoom_step",
BASE_PROPERTY_NAMES
]
}
fn command(&mut self, name: &str) -> Result<(), CapabilityAccessError> {
match name {
"clear" => {
self.clear();
Ok(())
}
"set_zoom" | "set_center" => Err(CapabilityAccessError::OutOfRange),
_ => Err(CapabilityAccessError::UnknownCommand),
}
}
}
impl Draw for Canvas {
fn draw(&mut self, context: &mut RenderContext) {
let rect = self.base.geometry();
let style = self.base.style();
let theme = crate::style::resolved_theme_style_shared("canvas");
let background = style
.background_color
.or_else(|| theme.as_ref().and_then(|t| t.background_color))
.unwrap_or(Color::WHITE);
let border = style
.border_color
.or_else(|| theme.as_ref().and_then(|t| t.border_color))
.unwrap_or_else(|| background.blend(&Color::BLACK, 0.2));
context.face_with_gradient(
rect,
background,
self.style().background_gradient.as_ref(),
self.style().surface.unwrap_or_default(),
self.style().border_radius.unwrap_or(0),
Color::BLACK,
);
if self.show_grid && rect.width > 0 && rect.height > 0 {
let step = self.grid_step.max(1);
let grid_color = border.with_alpha(96);
let key = self.grid_cache_key
^ color_key(background)
^ color_key(border)
^ ((step as u64) << 48);
context.push_layer_group(rect, None, 1.0, Some(key));
let mut x = rect.x;
while x <= rect.x + rect.width as i32 {
context.draw_line(
Point::new(x, rect.y),
Point::new(x, rect.y + rect.height as i32),
grid_color,
);
x += step as i32;
}
let mut y = rect.y;
while y <= rect.y + rect.height as i32 {
context.draw_line(
Point::new(rect.x, y),
Point::new(rect.x + rect.width as i32, y),
grid_color,
);
y += step as i32;
}
context.pop_layer();
}
let transform = self.transform;
for layer in &mut self.layers {
if !layer.visible || layer.commands.is_empty() {
continue;
}
let key = layer.cache_key;
context.push_layer_group(rect, None, 1.0, Some(key));
for stored in &mut layer.commands {
let command = stored.replay_under(transform);
if let Some(bounds) = command_bounds(command) {
if !rects_intersect(rect, bounds) {
continue;
}
}
context.execute_command_ref(command);
}
context.pop_layer();
}
crate::widget::draw::stroke_surface_border(
context,
rect,
self.style().border_radius.unwrap_or(0),
border,
1,
);
}
fn uses_custom_drawing(&self) -> bool {
true
}
}
impl EventHandler for Canvas {
fn handle_event(&mut self, event: &Event) {
self.base.handle_event(event);
if !self.base.is_enabled() {
return;
}
match event {
Event::MousePress { pos, button, .. } if *button == 1 => {
self.last_mouse_pos = *pos;
self.base.set_mouse_pressed(true);
self.mouse_pressed.emit_at(*pos, *button);
if let Some(index) = self.hit_test(*pos) {
self.command_picked.emit(index);
}
}
Event::MouseRelease { pos, button } if *button == 1 => {
self.last_mouse_pos = *pos;
self.base.set_mouse_pressed(false);
self.mouse_released.emit_at(*pos, *button);
}
Event::MouseMove { pos } if self.geometry().contains(*pos) => {
self.last_mouse_pos = *pos;
self.mouse_moved.emit(*pos);
}
Event::MouseDoubleClick { pos, button } if *button == 1 => {
self.last_mouse_pos = *pos;
self.double_clicked.emit_at(*pos, *button);
}
Event::Wheel { delta, .. } if self.zoom_enabled && delta.y != 0 => {
let anchor = self.last_mouse_pos;
let factor = if delta.y > 0 { 1.0 / self.zoom_step } else { self.zoom_step };
let previous = self.transform.scale;
let scale = (previous * factor).clamp(self.zoom_min, self.zoom_max);
if scale != previous {
let ratio = scale / previous;
self.transform.offset = Point::new(
anchor.x
- ((anchor.x - self.transform.offset.x) as f32 * ratio).round() as i32,
anchor.y
- ((anchor.y - self.transform.offset.y) as f32 * ratio).round() as i32,
);
self.transform.scale = scale;
self.invalidate_replay_caches();
self.zoom_changed.emit(scale);
self.base.request_redraw();
}
}
Event::ContextMenu { pos } => {
self.context_menu_requested.emit(*pos);
}
Event::KeyPress { key, modifiers } => {
let modifiers = crate::shortcut::Modifiers::from_event_bits(*modifiers);
if modifiers.contains(crate::shortcut::Modifiers::PRIMARY) {
match crate::shortcut::Key::from_key_code(*key) {
Some(crate::shortcut::Key::Z) => {
self.undo();
}
Some(crate::shortcut::Key::Y) => {
self.redo();
}
_ => {}
}
}
}
_ => { }
}
}
}
fn snap_scalar(value: i32, step: i32) -> i32 {
let step = step.max(1);
(value as f32 / step as f32).round() as i32 * step
}
fn color_key(color: Color) -> u64 {
(u64::from(color.r) << 24)
| (u64::from(color.g) << 16)
| (u64::from(color.b) << 8)
| u64::from(color.a)
}
fn transform_command(command: &RenderCommand, transform: CanvasTransform) -> RenderCommand {
let width = |value: u32| (value as f32 * transform.scale).round().max(1.0) as u32;
match command {
RenderCommand::FillRect { rect, color } => {
RenderCommand::FillRect { rect: transform.rect(*rect), color: *color }
}
RenderCommand::DrawRect { rect, color } => {
RenderCommand::DrawRect { rect: transform.rect(*rect), color: *color }
}
RenderCommand::FillRoundedRect { rect, radius, color } => RenderCommand::FillRoundedRect {
rect: transform.rect(*rect),
radius: width(*radius),
color: *color,
},
RenderCommand::DrawLine { from, to, color } => RenderCommand::DrawLine {
from: transform.point(*from),
to: transform.point(*to),
color: *color,
},
RenderCommand::DrawLineAA { from, to, color } => RenderCommand::DrawLineAA {
from: transform.point(*from),
to: transform.point(*to),
color: *color,
},
RenderCommand::DrawLineStroke { from, to, color, width: stroke } => {
RenderCommand::DrawLineStroke {
from: transform.point(*from),
to: transform.point(*to),
color: *color,
width: width(*stroke),
}
}
RenderCommand::FillCircle { center, radius, color } => RenderCommand::FillCircle {
center: transform.point(*center),
radius: width(*radius),
color: *color,
},
RenderCommand::FillCircleAA { center, radius, color } => RenderCommand::FillCircleAA {
center: transform.point(*center),
radius: width(*radius),
color: *color,
},
RenderCommand::DrawCircle { center, radius, color } => RenderCommand::DrawCircle {
center: transform.point(*center),
radius: width(*radius),
color: *color,
},
RenderCommand::DrawCircleStroke { center, radius, color, width: stroke } => {
RenderCommand::DrawCircleStroke {
center: transform.point(*center),
radius: width(*radius),
color: *color,
width: width(*stroke),
}
}
RenderCommand::DrawArc { center, radius, start_angle, end_angle, color, filled } => {
RenderCommand::DrawArc {
center: transform.point(*center),
radius: width(*radius),
start_angle: *start_angle,
end_angle: *end_angle,
color: *color,
filled: *filled,
}
}
RenderCommand::DrawPath { points, closed, color, filled, width: stroke } => {
RenderCommand::DrawPath {
points: points
.iter()
.map(|point| transform.point(*point))
.collect::<Vec<_>>()
.into(),
closed: *closed,
color: *color,
filled: *filled,
width: width(*stroke),
}
}
RenderCommand::DrawText { origin, text, font, color, alignment } => {
RenderCommand::DrawText {
origin: transform.point(*origin),
text: Arc::clone(text),
font: font.clone(),
color: *color,
alignment: *alignment,
}
}
RenderCommand::DrawImage { x, y, width: w, height: h, data } => RenderCommand::DrawImage {
x: transform.point(Point::new(*x, *y)).x,
y: transform.point(Point::new(*x, *y)).y,
width: width(*w),
height: width(*h),
data: Arc::clone(data),
},
other => other.clone(),
}
}
fn snap_point(point: Point, step: i32) -> Point {
Point::new(snap_scalar(point.x, step), snap_scalar(point.y, step))
}
fn snap_rect(rect: Rect, step: i32) -> Rect {
let step = step.max(1);
Rect::new(
snap_scalar(rect.x, step),
snap_scalar(rect.y, step),
snap_scalar(rect.width as i32, step).max(1) as u32,
snap_scalar(rect.height as i32, step).max(1) as u32,
)
}
fn snap_command(command: RenderCommand, step: i32) -> RenderCommand {
match command {
RenderCommand::FillRect { rect, color } => {
RenderCommand::FillRect { rect: snap_rect(rect, step), color }
}
RenderCommand::DrawRect { rect, color } => {
RenderCommand::DrawRect { rect: snap_rect(rect, step), color }
}
RenderCommand::FillRoundedRect { rect, radius, color } => {
RenderCommand::FillRoundedRect { rect: snap_rect(rect, step), radius, color }
}
RenderCommand::DrawLine { from, to, color } => RenderCommand::DrawLine {
from: snap_point(from, step),
to: snap_point(to, step),
color,
},
RenderCommand::DrawLineAA { from, to, color } => RenderCommand::DrawLineAA {
from: snap_point(from, step),
to: snap_point(to, step),
color,
},
RenderCommand::DrawLineStroke { from, to, color, width } => RenderCommand::DrawLineStroke {
from: snap_point(from, step),
to: snap_point(to, step),
color,
width,
},
RenderCommand::FillCircle { center, radius, color } => {
RenderCommand::FillCircle { center: snap_point(center, step), radius, color }
}
RenderCommand::FillCircleAA { center, radius, color } => {
RenderCommand::FillCircleAA { center: snap_point(center, step), radius, color }
}
RenderCommand::DrawCircle { center, radius, color } => {
RenderCommand::DrawCircle { center: snap_point(center, step), radius, color }
}
RenderCommand::DrawCircleStroke { center, radius, color, width } => {
RenderCommand::DrawCircleStroke {
center: snap_point(center, step),
radius,
color,
width,
}
}
RenderCommand::DrawArc { center, radius, start_angle, end_angle, color, filled } => {
RenderCommand::DrawArc {
center: snap_point(center, step),
radius,
start_angle,
end_angle,
color,
filled,
}
}
RenderCommand::DrawImage { x, y, width, height, data } => {
let rect = snap_rect(Rect::new(x, y, width, height), step);
RenderCommand::DrawImage {
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
data,
}
}
RenderCommand::DrawPath { points, closed, color, filled, width } => {
RenderCommand::DrawPath {
points: points
.iter()
.map(|point| snap_point(*point, step))
.collect::<Vec<_>>()
.into(),
closed,
color,
filled,
width,
}
}
RenderCommand::DrawText { origin, text, font, color, alignment } => {
RenderCommand::DrawText {
origin: snap_point(origin, step),
text,
font,
color,
alignment,
}
}
other => other,
}
}
fn without_anti_aliasing(command: RenderCommand) -> RenderCommand {
match command {
RenderCommand::DrawLineAA { from, to, color } => {
RenderCommand::DrawLine { from, to, color }
}
RenderCommand::FillCircleAA { center, radius, color } => {
RenderCommand::FillCircle { center, radius, color }
}
other => other,
}
}
fn command_bounds(command: &RenderCommand) -> Option<Rect> {
match command {
RenderCommand::FillRect { rect, .. } | RenderCommand::DrawRect { rect, .. } => Some(*rect),
RenderCommand::FillRoundedRect { rect, .. } => Some(*rect),
RenderCommand::FillCircle { center, radius, .. }
| RenderCommand::FillCircleAA { center, radius, .. }
| RenderCommand::DrawCircle { center, radius, .. }
| RenderCommand::DrawCircleStroke { center, radius, .. } => Some(Rect::new(
center.x - *radius as i32,
center.y - *radius as i32,
radius.saturating_mul(2).max(1),
radius.saturating_mul(2).max(1),
)),
RenderCommand::DrawLine { from, to, .. }
| RenderCommand::DrawLineAA { from, to, .. }
| RenderCommand::DrawLineStroke { from, to, .. } => Some(Rect::new(
from.x.min(to.x),
from.y.min(to.y),
(from.x - to.x).unsigned_abs().max(1),
(from.y - to.y).unsigned_abs().max(1),
)),
RenderCommand::DrawPath { points, .. } => {
let first = points.first()?;
let (mut left, mut top, mut right, mut bottom) = (first.x, first.y, first.x, first.y);
for point in points.iter() {
left = left.min(point.x);
top = top.min(point.y);
right = right.max(point.x);
bottom = bottom.max(point.y);
}
Some(Rect::new(
left,
top,
(right - left).unsigned_abs().max(1),
(bottom - top).unsigned_abs().max(1),
))
}
RenderCommand::DrawArc { center, radius, .. } => Some(Rect::new(
center.x - *radius as i32,
center.y - *radius as i32,
radius.saturating_mul(2).max(1),
radius.saturating_mul(2).max(1),
)),
RenderCommand::DrawImage { x, y, width, height, .. } => {
Some(Rect::new(*x, *y, *width, *height))
}
_ => None,
}
}
fn rects_intersect(a: Rect, b: Rect) -> bool {
a.x < b.x + b.width as i32
&& b.x < a.x + a.width as i32
&& a.y < b.y + b.height as i32
&& b.y < a.y + a.height as i32
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::{Font, Rect};
use crate::widget::svg::render_to_svg;
#[test]
fn canvas_creation_defaults() {
let canvas = Canvas::new(Rect::new(0, 0, 200, 100));
assert_eq!(canvas.command_count(), 0);
assert_eq!(canvas.geometry(), Rect::new(0, 0, 200, 100));
assert_eq!(canvas.kind(), WidgetKind::Canvas);
}
#[test]
fn canvas_fill_rect_adds_command() {
let mut canvas = Canvas::new(Rect::new(0, 0, 200, 100));
canvas.fill_rect(Rect::new(10, 10, 50, 50), Color::BLUE);
assert_eq!(canvas.command_count(), 1);
}
#[test]
fn canvas_draw_line_and_circle() {
let mut canvas = Canvas::new(Rect::new(0, 0, 200, 100));
canvas.draw_line(Point::new(0, 0), Point::new(100, 100), Color::RED);
canvas.fill_circle(Point::new(50, 50), 10, Color::GREEN);
assert_eq!(canvas.command_count(), 2);
}
#[test]
fn canvas_clear_removes_all_commands() {
let mut canvas = Canvas::new(Rect::new(0, 0, 200, 100));
canvas.fill_rect(Rect::new(0, 0, 100, 100), Color::BLUE);
canvas.draw_line(Point::new(0, 0), Point::new(100, 100), Color::RED);
assert_eq!(canvas.command_count(), 2);
canvas.clear();
assert_eq!(canvas.command_count(), 0);
}
#[test]
fn canvas_draw_path_stores_commands() {
let mut canvas = Canvas::new(Rect::new(0, 0, 200, 100));
let points =
vec![Point::new(0, 0), Point::new(100, 0), Point::new(100, 100), Point::new(0, 100)];
canvas.draw_path(points, true, Color::BLUE, false, 2);
assert_eq!(canvas.command_count(), 1);
}
#[test]
fn canvas_draw_text_stores_command() {
let mut canvas = Canvas::new(Rect::new(0, 0, 200, 100));
canvas.draw_text(Point::new(10, 20), "Hello", &Font::default_ui(), Color::BLACK);
assert_eq!(canvas.command_count(), 1);
}
#[test]
fn canvas_draw_produces_svg_output() {
let mut canvas = Canvas::new(Rect::new(0, 0, 200, 100));
canvas.fill_rect(Rect::new(10, 10, 50, 50), Color::BLUE);
canvas.draw_line(Point::new(0, 0), Point::new(100, 100), Color::RED);
let svg = render_to_svg(&mut canvas);
assert!(svg.starts_with("<svg"));
assert!(svg.len() > 100);
}
#[test]
fn canvas_signal_accessors() {
let canvas = Canvas::new(Rect::new(0, 0, 200, 100));
let _ = &canvas.mouse_pressed;
let _ = &canvas.mouse_released;
let _ = &canvas.mouse_moved;
let _ = &canvas.double_clicked;
}
#[test]
fn canvas_last_mouse_position_tracked() {
let mut canvas = Canvas::new(Rect::new(0, 0, 200, 100));
assert_eq!(canvas.last_mouse_pos(), Point::new(0, 0));
canvas.handle_event(&Event::MouseMove { pos: Point::new(42, 73) });
assert_eq!(canvas.last_mouse_pos(), Point::new(42, 73));
}
#[test]
fn canvas_disabled_state_blocks_events() {
let mut canvas = Canvas::new(Rect::new(0, 0, 200, 100));
canvas.set_enabled(false);
canvas.handle_event(&Event::MousePress {
pos: Point::new(10, 10),
button: 1,
modifiers: 0,
});
assert_eq!(canvas.last_mouse_pos(), Point::new(0, 0));
}
#[test]
fn canvas_double_click_emits_signal() {
let mut canvas = Canvas::new(Rect::new(0, 0, 200, 100));
canvas.handle_event(&Event::MouseDoubleClick { pos: Point::new(50, 50), button: 1 });
assert_eq!(canvas.last_mouse_pos(), Point::new(50, 50));
}
#[test]
fn canvas_draw_arc_and_draw_circle_stroke() {
let mut canvas = Canvas::new(Rect::new(0, 0, 200, 100));
canvas.draw_arc(Point::new(100, 50), 30, 0.0, std::f32::consts::PI, Color::RED, false);
canvas.draw_circle_stroke(Point::new(50, 50), 20, Color::BLUE, 3);
assert_eq!(canvas.command_count(), 2);
}
#[test]
fn canvas_fill_rounded_rect_and_line_aa() {
let mut canvas = Canvas::new(Rect::new(0, 0, 200, 100));
canvas.fill_rounded_rect(Rect::new(5, 5, 50, 50), 8, Color::rgba(255, 0, 0, 128));
canvas.draw_line_aa(Point::new(0, 0), Point::new(100, 50), Color::GREEN);
assert_eq!(canvas.command_count(), 2);
}
#[test]
fn the_canvas_reports_hover_because_it_forwards_to_its_base() {
use crate::style::WidgetState;
let mut canvas = Canvas::new(Rect::new(0, 0, 200, 100));
assert_eq!(canvas.widget_state(), WidgetState::Normal);
canvas.handle_event(&Event::MouseEnter { pos: Point::new(10, 10) });
assert_eq!(
canvas.widget_state(),
WidgetState::Hover,
"the base must have been told the pointer arrived"
);
canvas.handle_event(&Event::MouseLeave { pos: Point::new(500, 500) });
assert_eq!(canvas.widget_state(), WidgetState::Normal, "and that it left");
}
#[test]
fn canvas_layers_order_visibility_and_removal() {
let mut canvas = Canvas::new(Rect::new(0, 0, 200, 100));
canvas.begin_layer("background");
canvas.fill_rect(Rect::new(0, 0, 50, 50), Color::BLUE);
canvas.begin_layer("foreground");
canvas.fill_rect(Rect::new(10, 10, 50, 50), Color::RED);
assert_eq!(canvas.layer_count(), 2);
assert_eq!(canvas.command_count(), 2);
assert_eq!(canvas.hit_test(Point::new(20, 20)), Some(1));
canvas.set_layer_visible(1, false);
assert_eq!(canvas.hit_test(Point::new(20, 20)), Some(0), "a hidden layer is skipped");
assert!(canvas.set_layer_visible(1, true));
assert!(canvas.move_layer(1, 0));
assert_eq!(canvas.hit_test(Point::new(20, 20)), Some(1), "the background is on top now");
assert!(canvas.remove_layer(0));
assert_eq!(canvas.layer_count(), 1);
assert_eq!(canvas.command_count(), 1);
}
#[test]
fn canvas_groups_are_one_undo_step() {
let mut canvas = Canvas::new(Rect::new(0, 0, 200, 100));
assert!(canvas.begin_group());
canvas.draw_line(Point::new(0, 0), Point::new(5, 5), Color::RED);
canvas.draw_line(Point::new(5, 5), Point::new(10, 0), Color::RED);
assert!(canvas.end_group());
assert_eq!(canvas.undo_count(), 1, "a stroke is one history step");
assert!(canvas.undo());
assert_eq!(canvas.command_count(), 0);
assert!(canvas.redo());
assert_eq!(canvas.command_count(), 2);
}
#[test]
fn canvas_redo_history_is_bounded() {
let mut canvas = Canvas::new(Rect::new(0, 0, 200, 100));
canvas.set_command_limit(3);
for index in 0..6 {
canvas.fill_rect(Rect::new(index * 2, 0, 4, 4), Color::BLUE);
}
assert_eq!(canvas.command_count(), 3);
while canvas.undo() {}
assert!(canvas.redo_count() <= 3, "the redo stack obeys the command limit");
}
#[test]
fn canvas_global_anti_alias_degrades_aa_commands() {
let mut canvas = Canvas::new(Rect::new(0, 0, 200, 100));
canvas.set_anti_alias(false);
canvas.draw_line_aa(Point::new(0, 0), Point::new(10, 10), Color::RED);
canvas.fill_circle_aa(Point::new(20, 20), 5, Color::BLUE);
assert!(matches!(canvas.layers[0].commands[0].command, RenderCommand::DrawLine { .. }));
assert!(matches!(canvas.layers[0].commands[1].command, RenderCommand::FillCircle { .. }));
}
#[test]
fn canvas_snapping_rounds_stored_coordinates() {
let mut canvas = Canvas::new(Rect::new(0, 0, 200, 100));
canvas.set_grid_step(10);
canvas.set_snap_to_grid(true);
canvas.fill_rect(Rect::new(12, 18, 5, 5), Color::BLUE);
match canvas.layers[0].commands[0].command {
RenderCommand::FillRect { rect, .. } => {
assert_eq!((rect.x, rect.y), (10, 20));
}
_ => panic!("expected a filled rectangle"),
}
}
#[test]
fn canvas_export_png_produces_a_png() {
let mut canvas = Canvas::new(Rect::new(0, 0, 40, 30));
canvas.fill_rect(Rect::new(5, 5, 10, 10), Color::RED);
let bytes = canvas.export_png(40, 30, 1.0).expect("a valid export");
assert!(bytes.starts_with(b"\x89PNG"));
}
#[test]
fn canvas_new_properties_round_trip() {
let mut canvas = Canvas::new(Rect::new(0, 0, 200, 100));
assert_eq!(canvas.get("anti_alias").unwrap(), CapabilityValue::Bool(true));
canvas.set("anti_alias", CapabilityValue::Bool(false)).unwrap();
assert!(!canvas.anti_alias());
canvas.set("show_grid", CapabilityValue::Bool(true)).unwrap();
assert!(canvas.show_grid());
canvas
.set(
"zoom_range",
CapabilityValue::Tuple(crate::compat::Vec::from([
CapabilityValue::Float(0.5),
CapabilityValue::Float(8.0),
])),
)
.unwrap();
assert_eq!(canvas.zoom_range(), (0.5, 8.0));
}
}