Skip to content
Open
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
4 changes: 3 additions & 1 deletion src/app_state.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::canvas::{CanvasTransform, Uniforms};
use crate::document::Document;
use crate::drawing::{Element, ElementId, Tool, sync_id_counters};
use crate::drawing::{Element, ElementId, FillStyle, Tool, sync_id_counters};
use crate::history::{Action, History};
use crate::state::{
Canvas, ColorPickerState, GeometryBuffers, GpuContext, InputState, SdfBuffers, SelectionState,
Expand Down Expand Up @@ -40,6 +40,7 @@ pub struct State {
pub current_color: [f32; 4],
pub color_picker: ColorPickerState,
pub stroke_width: f32,
pub current_fill_style: FillStyle,
pub clipboard: Vec<Element>,

pub ui_renderer: UiRenderer,
Expand Down Expand Up @@ -425,6 +426,7 @@ impl State {
current_color: [0.0, 0.0, 0.0, 1.0],
color_picker: ColorPickerState::new(),
stroke_width: 2.0,
current_fill_style: FillStyle::None,
clipboard: Vec::new(),
ui_renderer,
text_renderer,
Expand Down
8 changes: 4 additions & 4 deletions src/document.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize};

use crate::drawing::Element;
use crate::drawing::{Element, FillStyle};

pub const SCHEMA_VERSION: u32 = 1;

Expand Down Expand Up @@ -106,7 +106,7 @@ mod tests {
position: [50.0, 50.0],
size: [200.0, 100.0],
color: [0.0, 1.0, 0.0, 1.0],
fill: true,
fill_style: FillStyle::Solid,
stroke_width: 1.5,
rough_style: None,
},
Expand All @@ -118,7 +118,7 @@ mod tests {
center: [150.0, 150.0],
radius: 75.0,
color: [0.0, 0.0, 1.0, 1.0],
fill: false,
fill_style: FillStyle::None,
stroke_width: 2.0,
rough_style: None,
},
Expand All @@ -130,7 +130,7 @@ mod tests {
position: [300.0, 100.0],
size: [80.0, 60.0],
color: [1.0, 1.0, 0.0, 1.0],
fill: false,
fill_style: FillStyle::None,
stroke_width: 2.5,
rough_style: None,
},
Expand Down
104 changes: 90 additions & 14 deletions src/drawing.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,70 @@
use serde::{Deserialize, Serialize};
use serde::{Deserialize, Deserializer, Serialize};
use std::sync::atomic::{AtomicU64, Ordering};

/// Fill style for shape primitives, following Excalidraw's approach.
///
/// - `None`: No fill (stroke only)
/// - `Solid`: Solid color fill
/// - `Hachure`: Parallel sketchy lines at an angle (default Excalidraw style)
/// - `CrossHatch`: Two sets of hachure lines at perpendicular angles
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum FillStyle {
None,
Solid,
Hachure,
CrossHatch,
}

impl FillStyle {
/// Cycle to the next fill style: None -> Hachure -> CrossHatch -> Solid -> None
pub fn next(self) -> Self {
match self {
FillStyle::None => FillStyle::Hachure,
FillStyle::Hachure => FillStyle::CrossHatch,
FillStyle::CrossHatch => FillStyle::Solid,
FillStyle::Solid => FillStyle::None,
}
}

pub fn is_filled(self) -> bool {
self != FillStyle::None
}
}

/// Custom deserializer that handles both old `fill: bool` and new `fill_style: "Hachure"` formats.
pub fn deserialize_fill_style<'de, D>(deserializer: D) -> Result<FillStyle, D::Error>
where
D: Deserializer<'de>,
{
use serde::de;

struct FillStyleVisitor;

impl<'de> de::Visitor<'de> for FillStyleVisitor {
type Value = FillStyle;

fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a FillStyle string or a boolean")
}

fn visit_bool<E: de::Error>(self, v: bool) -> Result<FillStyle, E> {
Ok(if v { FillStyle::Solid } else { FillStyle::None })
}

fn visit_str<E: de::Error>(self, v: &str) -> Result<FillStyle, E> {
match v {
"None" => Ok(FillStyle::None),
"Solid" => Ok(FillStyle::Solid),
"Hachure" => Ok(FillStyle::Hachure),
"CrossHatch" => Ok(FillStyle::CrossHatch),
_ => Err(de::Error::unknown_variant(v, &["None", "Solid", "Hachure", "CrossHatch"])),
}
}
}

deserializer.deserialize_any(FillStyleVisitor)
}

static NEXT_ELEMENT_ID: AtomicU64 = AtomicU64::new(1);
static NEXT_GROUP_ID: AtomicU64 = AtomicU64::new(1);

Expand Down Expand Up @@ -105,23 +169,26 @@ pub enum DrawingElement {
position: [f32; 2],
size: [f32; 2],
color: [f32; 4],
fill: bool,
#[serde(deserialize_with = "deserialize_fill_style", alias = "fill")]
fill_style: FillStyle,
stroke_width: f32,
rough_style: Option<crate::rough::RoughOptions>,
},
Circle {
center: [f32; 2],
radius: f32,
color: [f32; 4],
fill: bool,
#[serde(deserialize_with = "deserialize_fill_style", alias = "fill")]
fill_style: FillStyle,
stroke_width: f32,
rough_style: Option<crate::rough::RoughOptions>,
},
Diamond {
position: [f32; 2],
size: [f32; 2],
color: [f32; 4],
fill: bool,
#[serde(deserialize_with = "deserialize_fill_style", alias = "fill")]
fill_style: FillStyle,
stroke_width: f32,
rough_style: Option<crate::rough::RoughOptions>,
},
Expand Down Expand Up @@ -176,24 +243,33 @@ impl DrawingElement {
}
}

pub fn set_fill(&mut self, fill: bool) -> bool {
pub fn fill_style(&self) -> Option<FillStyle> {
match self {
DrawingElement::Rectangle { fill_style, .. }
| DrawingElement::Circle { fill_style, .. }
| DrawingElement::Diamond { fill_style, .. } => Some(*fill_style),
_ => None,
}
}

pub fn set_fill_style(&mut self, style: FillStyle) -> bool {
match self {
DrawingElement::Rectangle { fill: value, .. }
| DrawingElement::Circle { fill: value, .. }
| DrawingElement::Diamond { fill: value, .. } => {
*value = fill;
DrawingElement::Rectangle { fill_style, .. }
| DrawingElement::Circle { fill_style, .. }
| DrawingElement::Diamond { fill_style, .. } => {
*fill_style = style;
true
}
_ => false,
}
}

pub fn toggle_fill(&mut self) -> bool {
pub fn cycle_fill_style(&mut self) -> bool {
match self {
DrawingElement::Rectangle { fill, .. }
| DrawingElement::Circle { fill, .. }
| DrawingElement::Diamond { fill, .. } => {
*fill = !*fill;
DrawingElement::Rectangle { fill_style, .. }
| DrawingElement::Circle { fill_style, .. }
| DrawingElement::Diamond { fill_style, .. } => {
*fill_style = fill_style.next();
true
}
_ => false,
Expand Down
58 changes: 47 additions & 11 deletions src/event_handler.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use crate::app_state::State;
use crate::drawing::{BoxState, DrawingElement, Element, ElementId, GroupId, Tool};
use crate::drawing::{BoxState, DrawingElement, Element, ElementId, FillStyle, GroupId, Tool};
use crate::history::Action;
use crate::state::ResizeHandle;
use crate::state::UserInputState::{Dragging, Drawing, Idle, MarqueeSelecting, Panning, Resizing};
use crate::ui::ColorInteraction;
use crate::ui::{ColorInteraction, FillInteraction};
use crate::update_logic::handle_positions;
use rand::Rng;
use winit::event::*;
Expand Down Expand Up @@ -151,10 +151,25 @@ impl State {
}
}

match self.ui_renderer.handle_fill_interaction(
self.input.mouse_pos,
self.current_tool,
(self.size.width as f32, self.size.height as f32),
) {
FillInteraction::None => {}
FillInteraction::SelectFill(style) => {
self.current_fill_style = style;
// Also apply to selected elements
self.set_fill_style_on_selection(style);
return true;
}
}

if self.ui_renderer.is_mouse_over_ui(
self.input.mouse_pos,
(self.size.width as f32, self.size.height as f32),
&self.color_picker,
self.current_tool,
) || self.is_mouse_in_titlebar(self.input.mouse_pos)
{
return true;
Expand Down Expand Up @@ -253,6 +268,7 @@ impl State {
self.input.mouse_pos,
(self.size.width as f32, self.size.height as f32),
&self.color_picker,
self.current_tool,
) || self.is_mouse_in_titlebar(self.input.mouse_pos)
{
self.finish_drawing();
Expand Down Expand Up @@ -320,7 +336,7 @@ impl State {
}
}
KeyCode::KeyF => {
self.toggle_fill_on_selection();
self.cycle_fill_on_selection();
true
}
KeyCode::BracketLeft => {
Expand Down Expand Up @@ -829,7 +845,27 @@ impl State {
}
}

fn toggle_fill_on_selection(&mut self) {
fn cycle_fill_on_selection(&mut self) {
let ids = self.input.selection.selected_ids.clone();
if ids.is_empty() {
// Cycle the default fill style when nothing is selected
self.current_fill_style = self.current_fill_style.next();
return;
}
let before = self.snapshot_elements(&ids);
let mut changed = false;
for id in &ids {
if let Some(element) = self.find_element_mut_by_id(*id) {
changed |= element.shape.cycle_fill_style();
}
}
if changed {
let after = self.snapshot_elements(&ids);
self.record_action(Action::ModifyProperty { before, after });
}
}

fn set_fill_style_on_selection(&mut self, style: FillStyle) {
let ids = self.input.selection.selected_ids.clone();
if ids.is_empty() {
return;
Expand All @@ -838,7 +874,7 @@ impl State {
let mut changed = false;
for id in &ids {
if let Some(element) = self.find_element_mut_by_id(*id) {
changed |= element.shape.toggle_fill();
changed |= element.shape.set_fill_style(style);
}
}
if changed {
Expand Down Expand Up @@ -1200,7 +1236,7 @@ impl State {
position,
size,
color: self.current_color,
fill: false,
fill_style: self.current_fill_style,
stroke_width: self.stroke_width,
rough_style: Some(rough_style),
})
Expand All @@ -1215,7 +1251,7 @@ impl State {
center: start,
radius,
color: self.current_color,
fill: false,
fill_style: self.current_fill_style,
stroke_width: self.stroke_width,
rough_style: Some(rough_options),
}))
Expand All @@ -1230,7 +1266,7 @@ impl State {
position,
size,
color: self.current_color,
fill: false,
fill_style: self.current_fill_style,
stroke_width: self.stroke_width,
rough_style: Some(rough_style),
})
Expand Down Expand Up @@ -1331,7 +1367,7 @@ impl State {
self.current_color[2],
0.5,
],
fill: false,
fill_style: self.current_fill_style,
stroke_width: self.stroke_width,
rough_style: None,
});
Expand All @@ -1350,7 +1386,7 @@ impl State {
self.current_color[2],
0.5,
],
fill: false,
fill_style: self.current_fill_style,
stroke_width: self.stroke_width,
rough_style: None,
});
Expand Down Expand Up @@ -1404,7 +1440,7 @@ impl State {
self.current_color[2],
0.5,
],
fill: false,
fill_style: self.current_fill_style,
stroke_width: self.stroke_width,
rough_style: None,
});
Expand Down
Loading