Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
7ae2688
init + most basic proof of concept
anesthetice Sep 8, 2024
a62abad
basics (re-init of sorts)
anesthetice Sep 10, 2024
e6a4eb9
basic logic, re-ordered shaper menu bar
anesthetice Sep 10, 2024
f0dac3a
major ui changes, wiring things together, still not working, need to …
anesthetice Sep 10, 2024
aa16702
haha funny crash when zooming in haha so funny (re-rendering is threa…
anesthetice Sep 10, 2024
74e1fc0
working pretty well, still missing docs and logic not fully there yet
anesthetice Sep 13, 2024
56dae02
removed Fragile, bug fixes, styled stroke for other shapes (all excep…
anesthetice Sep 15, 2024
1582b96
basic documentation
anesthetice Sep 15, 2024
2c4d36a
Merge branch 'main' into styled-lines
anesthetice Sep 17, 2024
a3adec1
finished dealing with the updated deps
anesthetice Sep 17, 2024
783b845
removed unsafe, less efficient method applied (but I mean it's not th…
anesthetice Oct 8, 2024
d68e110
Merge branch 'main' into styled-lines
anesthetice Feb 17, 2025
7176cd9
cleaner implementation now that piet uses Arc<>, just missing a coupl…
anesthetice Feb 18, 2025
e007b56
marker works for shapes, the preview doesn't, will probably move to u…
anesthetice Feb 18, 2025
1a60aa5
merged main
anesthetice Feb 27, 2025
b7ce093
lazy change to highlighter (work in progress)
anesthetice Feb 27, 2025
1c3123b
shaper brush type
anesthetice Mar 2, 2025
0eae12b
editable highlighter opacity
anesthetice Mar 9, 2025
a4cb107
bugfixes, polyline support
anesthetice Mar 11, 2025
230cc04
testing + infinitesimally small amount of documentation
anesthetice Mar 11, 2025
b25bf16
integrating highlighter into shaperstyle doesn't work very well in th…
anesthetice Apr 5, 2025
df6b673
highlight mode toggle instead of ShaperStyle, hide rough options when…
anesthetice Apr 5, 2025
0eaaba0
Merge branch 'main' into styled-lines
anesthetice Apr 7, 2025
92a5f90
merged main
anesthetice Aug 25, 2025
138dc49
if fill color is fully transparent, highlighter mode won't forcefully…
anesthetice Aug 25, 2025
dd2171a
chore: consistent serialization naming for new types
flxzt Aug 31, 2025
947ac13
chore: minor cosmetic code changes, format UI file
flxzt Aug 31, 2025
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
58 changes: 45 additions & 13 deletions crates/rnote-compose/src/style/smooth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
mod smoothoptions;

// Re-exports
pub use smoothoptions::SmoothOptions;
pub use smoothoptions::{LineCap, LineStyle, SmoothOptions};

// Imports
use super::Composer;
Expand All @@ -26,7 +26,12 @@ impl Composer<SmoothOptions> for Line {

if let Some(stroke_color) = options.stroke_color {
let stroke_brush = cx.solid_brush(stroke_color.into());
cx.stroke(line, &stroke_brush, options.stroke_width);
cx.stroke_styled(
line,
&stroke_brush,
options.stroke_width,
&options.piet_stroke_style,
);
}
cx.restore().unwrap();
}
Expand All @@ -43,10 +48,11 @@ impl Composer<SmoothOptions> for Arrow {

if let Some(stroke_color) = options.stroke_color {
let arrow = self.to_kurbo(Some(options.stroke_width));
cx.stroke(
cx.stroke_styled(
arrow,
&Into::<piet::Color>::into(stroke_color),
options.stroke_width,
&options.piet_stroke_style,
);
}

Expand All @@ -70,7 +76,12 @@ impl Composer<SmoothOptions> for Rectangle {

if let Some(stroke_color) = options.stroke_color {
let stroke_brush = cx.solid_brush(stroke_color.into());
cx.stroke(shape, &stroke_brush, options.stroke_width);
cx.stroke_styled(
shape,
&stroke_brush,
options.stroke_width,
&options.piet_stroke_style,
);
}
cx.restore().unwrap();
}
Expand All @@ -92,7 +103,12 @@ impl Composer<SmoothOptions> for Ellipse {

if let Some(stroke_color) = options.stroke_color {
let stroke_brush = cx.solid_brush(stroke_color.into());
cx.stroke(ellipse, &stroke_brush, options.stroke_width);
cx.stroke_styled(
ellipse,
&stroke_brush,
options.stroke_width,
&options.piet_stroke_style,
);
}
cx.restore().unwrap();
}
Expand All @@ -114,7 +130,12 @@ impl Composer<SmoothOptions> for QuadraticBezier {

if let Some(stroke_color) = options.stroke_color {
let stroke_brush = cx.solid_brush(stroke_color.into());
cx.stroke(quadbez, &stroke_brush, options.stroke_width);
cx.stroke_styled(
quadbez,
&stroke_brush,
options.stroke_width,
&options.piet_stroke_style,
);
}
cx.restore().unwrap();
}
Expand All @@ -136,7 +157,12 @@ impl Composer<SmoothOptions> for CubicBezier {

if let Some(stroke_color) = options.stroke_color {
let stroke_brush = cx.solid_brush(stroke_color.into());
cx.stroke(cubbez, &stroke_brush, options.stroke_width);
cx.stroke_styled(
cubbez,
&stroke_brush,
options.stroke_width,
&options.piet_stroke_style,
);
}
cx.restore().unwrap();
}
Expand All @@ -161,13 +187,16 @@ impl Composer<SmoothOptions> for Polyline {
&Into::<piet::Color>::into(color),
);
} else {
let style = options
.piet_stroke_style
.clone()
.line_cap(piet::LineCap::Butt)
.line_join(piet::LineJoin::Bevel);
cx.stroke_styled(
self.outline_path(),
&Into::<piet::Color>::into(color),
options.stroke_width,
&piet::StrokeStyle::default()
.line_cap(piet::LineCap::Butt)
.line_join(piet::LineJoin::Bevel),
&style,
);
}
}
Expand Down Expand Up @@ -196,14 +225,17 @@ impl Composer<SmoothOptions> for Polygon {
if let Some(fill_color) = options.fill_color {
cx.fill(&outline_path, &Into::<piet::Color>::into(fill_color));
}
let style = options
.piet_stroke_style
.clone()
.line_cap(piet::LineCap::Butt)
.line_join(piet::LineJoin::Bevel);

cx.stroke_styled(
&outline_path,
&Into::<piet::Color>::into(color),
options.stroke_width,
&piet::StrokeStyle::default()
.line_cap(piet::LineCap::Butt)
.line_join(piet::LineJoin::Bevel),
&style,
);
}
}
Expand Down
234 changes: 230 additions & 4 deletions crates/rnote-compose/src/style/smooth/smoothoptions.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
// Imports
use crate::Color;
use crate::style::PressureCurve;
use anyhow::Context;
use num_derive::{FromPrimitive, ToPrimitive};
use serde::{Deserialize, Serialize};
use std::{
f64,
ops::{AddAssign, MulAssign},
};

/// Options for shapes that can be drawn in a smooth style.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default, rename = "smooth_options")]
/// Options for shapes that can be drawn in a smooth style. Ensure the precursor struct used in deserialization matches this one.
#[derive(Debug, Clone, Serialize)]
#[serde(rename = "smooth_options")]
pub struct SmoothOptions {
/// Stroke width.
#[serde(rename = "stroke_width", with = "crate::serialize::f64_dp3")]
Expand All @@ -19,15 +25,235 @@ pub struct SmoothOptions {
/// Pressure curve.
#[serde(rename = "pressure_curve")]
pub pressure_curve: PressureCurve,
/// Line style.
#[serde(rename = "line_style")]
pub line_style: LineStyle,
/// Line cap.
#[serde(rename = "line_cap")]
pub line_cap: LineCap,
/// The inner piet::StrokeStyle, computed using the stroke_width, line_style, and line_cap.
#[serde(skip)]
pub piet_stroke_style: piet::StrokeStyle,
}

impl Default for SmoothOptions {
fn default() -> Self {
let stroke_width: f64 = 2.0;
let line_style = LineStyle::default();
let line_cap = LineCap::default();
Self {
stroke_width: 2.0,
stroke_width,
stroke_color: Some(Color::BLACK),
fill_color: None,
pressure_curve: PressureCurve::default(),
line_style,
line_cap,
piet_stroke_style: Self::compute_piet_stroke_style(stroke_width, line_style, line_cap),
}
}
}

impl SmoothOptions {
/// The ratio between the length of a dash and the width of the stroke
const DASH_LENGTH_TO_WIDTH_RATIO: f64 = f64::consts::E;

fn compute_piet_stroke_style(
stroke_width: f64,
line_style: LineStyle,
line_cap: LineCap,
) -> piet::StrokeStyle {
let mut dash_pattern = line_style.as_unscaled_vector();
match line_cap {
LineCap::Straight => dash_pattern
.iter_mut()
.for_each(|e| e.mul_assign(stroke_width * Self::DASH_LENGTH_TO_WIDTH_RATIO)),
LineCap::Rounded => dash_pattern.iter_mut().enumerate().for_each(|(idx, e)| {
if !line_style.is_dotted() {
e.mul_assign(stroke_width * Self::DASH_LENGTH_TO_WIDTH_RATIO);
}
// If the stroke has a rounded linecap, a half-disk with radius equal to the stroke width is added both ends of a stroke, this increases the length of each line by the width of the stroke, and is not taken into account by DashStroke, it has to be manually accounted for
if idx % 2 == 1 {
e.add_assign(2.0 * stroke_width)
}
}),
};
let mut stroke_style = piet::StrokeStyle::new();
stroke_style.set_dash_pattern(dash_pattern);
stroke_style.set_line_cap(line_cap.into());
stroke_style
}

/// Updates the inner piet::Strokestyle
pub fn update_piet_stroke_style(&mut self) {
self.piet_stroke_style =
Self::compute_piet_stroke_style(self.stroke_width, self.line_style, self.line_cap);
}

/// Updates the line cap
pub fn update_line_cap(&mut self, line_cap: LineCap) {
// Dotted style requires a round LineCap
if self.line_style.is_dotted() && line_cap != LineCap::Rounded {
self.line_style = LineStyle::Solid;
}
self.line_cap = line_cap;
self.update_piet_stroke_style();
}

/// Updates the line style
pub fn update_line_style(&mut self, line_style: LineStyle) {
// Dotted style requires a round LineCap
if line_style.is_dotted() {
self.line_cap = LineCap::Rounded;
}
self.line_style = line_style;
self.update_piet_stroke_style();
}
}

impl<'de> Deserialize<'de> for SmoothOptions {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(default, rename = "smooth_options")]
struct SmoothOptionsPrecursor {
#[serde(rename = "stroke_width", with = "crate::serialize::f64_dp3")]
pub stroke_width: f64,
#[serde(rename = "stroke_color")]
pub stroke_color: Option<Color>,
#[serde(rename = "fill_color")]
pub fill_color: Option<Color>,
#[serde(rename = "pressure_curve")]
pub pressure_curve: PressureCurve,
#[serde(rename = "line_style")]
pub line_style: LineStyle,
#[serde(rename = "line_cap")]
pub line_cap: LineCap,
}

impl From<SmoothOptions> for SmoothOptionsPrecursor {
fn from(value: SmoothOptions) -> Self {
Self {
stroke_width: value.stroke_width,
stroke_color: value.stroke_color,
fill_color: value.fill_color,
pressure_curve: value.pressure_curve,
line_style: value.line_style,
line_cap: value.line_cap,
}
}
}

impl Default for SmoothOptionsPrecursor {
fn default() -> Self {
SmoothOptions::default().into()
}
}

let precursor = SmoothOptionsPrecursor::deserialize(deserializer)?;

Ok(SmoothOptions {
stroke_width: precursor.stroke_width,
stroke_color: precursor.stroke_color,
fill_color: precursor.fill_color,
pressure_curve: precursor.pressure_curve,
line_style: precursor.line_style,
line_cap: precursor.line_cap,
piet_stroke_style: Self::compute_piet_stroke_style(
precursor.stroke_width,
precursor.line_style,
precursor.line_cap,
),
})
}
}

/// Line cap present at the start and end of a line
#[derive(
Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, FromPrimitive, ToPrimitive,
)]
#[serde(rename = "line_cap")]
pub enum LineCap {
/// Straight line cap
#[default]
#[serde(rename = "straight")]
Straight,
/// Rounded line cap
#[serde(rename = "rounded")]
Rounded,
}

impl TryFrom<u32> for LineCap {
type Error = anyhow::Error;

fn try_from(value: u32) -> Result<Self, Self::Error> {
num_traits::FromPrimitive::from_u32(value)
.with_context(|| format!("LineCap try_from::<u32>() for value {value} failed"))
}
}

impl From<LineCap> for piet::LineCap {
fn from(value: LineCap) -> Self {
match value {
LineCap::Straight => piet::LineCap::Butt,
LineCap::Rounded => piet::LineCap::Round,
}
}
}

/// The overall style of the line
#[derive(
Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, FromPrimitive, ToPrimitive,
)]
#[serde(rename = "line_style")]
pub enum LineStyle {
/// Solid line style
#[default]
#[serde(rename = "solid")]
Solid,
/// Dotted line style, the dots are equidistant
#[serde(rename = "dotted")]
Dotted,
/// Dashed line style, the dashes have less space between them
#[serde(rename = "dashed_narrow")]
DashedNarrow,
/// Dashed line style, the dashes are equidistant
#[serde(rename = "dashed_equidistant")]
DashedEquidistant,
/// Dashed line style, the dashes have more space between them
#[serde(rename = "dashed_wide")]
DashedWide,
}

impl LineStyle {
/// Returns the baseline (meaning unscaled) dash pattern
fn as_unscaled_vector(&self) -> Vec<f64> {
match self {
Self::Solid => Vec::new(),
Self::Dotted => vec![0.0, 0.0], // LineCap must be set to 'Rounded'
Self::DashedNarrow => vec![1.0, 0.618], // golden ratio, the longer segment is the dash itself
Self::DashedEquidistant => vec![1.0, 1.0],
Self::DashedWide => vec![1.0, 1.618], // golden ratio, the longer segment is the space between dashes
}
}
/// Indicates whether or not the LineStyle is dotted
pub fn is_dotted(&self) -> bool {
match self {
Self::Solid => false,
Self::Dotted => true,
Self::DashedNarrow => false,
Self::DashedEquidistant => false,
Self::DashedWide => false,
}
}
}

impl TryFrom<u32> for LineStyle {
type Error = anyhow::Error;

fn try_from(value: u32) -> Result<Self, Self::Error> {
num_traits::FromPrimitive::from_u32(value)
.with_context(|| format!("LineStyle try_from::<u32>() for value {value} failed"))
}
}
Loading