feat: Enhance brush settings and add new layer paste functionality

- Added brush color variant and variant amount settings to the brush panel.
- Introduced a checkbox for color variant and a slider for variant amount in the brush settings.
- Implemented "Paste as New Layer" functionality, allowing users to paste clipboard images directly onto a new layer.
- Updated menus to include the new paste option with a shortcut.
- Improved layer panel iconography by replacing text buttons with SVG icons for visibility and lock toggles.
- Created new SVG assets for closed eye and watercolor icons.
- Enhanced history panel to provide richer descriptions for brush strokes and vector shape modifications.
- Fixed the issue where the first brush stroke was missing from the history panel.
This commit is contained in:
2026-07-21 17:40:41 +03:00
parent e5d899d72d
commit 7b4073e55b
18 changed files with 1732 additions and 173 deletions
+292 -6
View File
@@ -1325,6 +1325,10 @@ impl Engine {
} else {
None
};
let desc = format!(
"Add Vector Shape ({})",
vector_shape_name_or_kind(&shape)
);
if let Some(layer) = self.document.active_layer_mut() {
if let LayerData::Vector { ref mut shapes } = layer.data {
shapes.push(shape);
@@ -1333,21 +1337,18 @@ impl Engine {
self.document.modified = true;
}
}
self.document.push_vector_snapshot(
layer_idx,
before_shapes,
"Add Vector Shape".to_string(),
);
self.document.push_vector_snapshot(layer_idx, before_shapes, desc);
return;
}
}
// Create the final vector layer before recording history. This keeps the first shape and
// its auto-created layer in one atomic Undo/Redo entry instead of capturing an empty
// intermediate raster layer.
let desc = format!("Add Vector Shape ({})", vector_shape_name_or_kind(&shape));
self.document.add_vector_layer_with_shape(
format!("Vector {}", self.document.layers.len()),
shape,
"Add Vector Shape",
&desc,
);
}
@@ -1395,6 +1396,14 @@ impl Engine {
}
pub fn set_vector_shape_color(&mut self, layer_id: u64, shape_idx: usize, color: [u8; 4]) {
let layer_idx = self.document.layer_index_by_id(layer_id);
let before_shapes = self
.document
.get_layer_by_id(layer_id)
.and_then(|l| match &l.data {
LayerData::Vector { shapes } => Some(shapes.clone()),
_ => None,
});
if let Some(layer) = self.document.get_layer_by_id_mut(layer_id) {
if let LayerData::Vector { ref mut shapes } = layer.data {
if let Some(shape) = shapes.get_mut(shape_idx) {
@@ -1405,9 +1414,33 @@ impl Engine {
}
}
}
if let (Some(idx), Some(before)) = (layer_idx, before_shapes) {
if let Some(shape) = self
.document
.get_layer_by_id(layer_id)
.and_then(|l| match &l.data {
LayerData::Vector { shapes } => shapes.get(shape_idx),
_ => None,
})
{
self.document.push_vector_snapshot(
idx,
Some(before),
format!("Set Stroke Color ({})", vector_shape_name_or_kind(shape)),
);
}
}
}
pub fn set_vector_shape_fill(&mut self, layer_id: u64, shape_idx: usize, fill: bool) {
let layer_idx = self.document.layer_index_by_id(layer_id);
let before_shapes = self
.document
.get_layer_by_id(layer_id)
.and_then(|l| match &l.data {
LayerData::Vector { shapes } => Some(shapes.clone()),
_ => None,
});
if let Some(layer) = self.document.get_layer_by_id_mut(layer_id) {
if let LayerData::Vector { ref mut shapes } = layer.data {
if let Some(shape) = shapes.get_mut(shape_idx) {
@@ -1420,9 +1453,33 @@ impl Engine {
}
}
}
if let (Some(idx), Some(before)) = (layer_idx, before_shapes) {
if let Some(shape) = self
.document
.get_layer_by_id(layer_id)
.and_then(|l| match &l.data {
LayerData::Vector { shapes } => shapes.get(shape_idx),
_ => None,
})
{
self.document.push_vector_snapshot(
idx,
Some(before),
format!("Toggle Fill ({})", vector_shape_name_or_kind(shape)),
);
}
}
}
pub fn set_vector_shape_fill_color(&mut self, layer_id: u64, shape_idx: usize, color: [u8; 4]) {
let layer_idx = self.document.layer_index_by_id(layer_id);
let before_shapes = self
.document
.get_layer_by_id(layer_id)
.and_then(|l| match &l.data {
LayerData::Vector { shapes } => Some(shapes.clone()),
_ => None,
});
if let Some(layer) = self.document.get_layer_by_id_mut(layer_id) {
if let LayerData::Vector { ref mut shapes } = layer.data {
if let Some(shape) = shapes.get_mut(shape_idx) {
@@ -1435,9 +1492,33 @@ impl Engine {
}
}
}
if let (Some(idx), Some(before)) = (layer_idx, before_shapes) {
if let Some(shape) = self
.document
.get_layer_by_id(layer_id)
.and_then(|l| match &l.data {
LayerData::Vector { shapes } => shapes.get(shape_idx),
_ => None,
})
{
self.document.push_vector_snapshot(
idx,
Some(before),
format!("Set Fill Color ({})", vector_shape_name_or_kind(shape)),
);
}
}
}
pub fn set_vector_shape_stroke(&mut self, layer_id: u64, shape_idx: usize, stroke: f32) {
let layer_idx = self.document.layer_index_by_id(layer_id);
let before_shapes = self
.document
.get_layer_by_id(layer_id)
.and_then(|l| match &l.data {
LayerData::Vector { shapes } => Some(shapes.clone()),
_ => None,
});
if let Some(layer) = self.document.get_layer_by_id_mut(layer_id) {
if let LayerData::Vector { ref mut shapes } = layer.data {
if let Some(shape) = shapes.get_mut(shape_idx) {
@@ -1448,9 +1529,33 @@ impl Engine {
}
}
}
if let (Some(idx), Some(before)) = (layer_idx, before_shapes) {
if let Some(shape) = self
.document
.get_layer_by_id(layer_id)
.and_then(|l| match &l.data {
LayerData::Vector { shapes } => shapes.get(shape_idx),
_ => None,
})
{
self.document.push_vector_snapshot(
idx,
Some(before),
format!("Set Stroke Width ({})", vector_shape_name_or_kind(shape)),
);
}
}
}
pub fn set_vector_shape_opacity(&mut self, layer_id: u64, shape_idx: usize, opacity: f32) {
let layer_idx = self.document.layer_index_by_id(layer_id);
let before_shapes = self
.document
.get_layer_by_id(layer_id)
.and_then(|l| match &l.data {
LayerData::Vector { shapes } => Some(shapes.clone()),
_ => None,
});
if let Some(layer) = self.document.get_layer_by_id_mut(layer_id) {
if let LayerData::Vector { ref mut shapes } = layer.data {
if let Some(shape) = shapes.get_mut(shape_idx) {
@@ -1461,6 +1566,22 @@ impl Engine {
}
}
}
if let (Some(idx), Some(before)) = (layer_idx, before_shapes) {
if let Some(shape) = self
.document
.get_layer_by_id(layer_id)
.and_then(|l| match &l.data {
LayerData::Vector { shapes } => shapes.get(shape_idx),
_ => None,
})
{
self.document.push_vector_snapshot(
idx,
Some(before),
format!("Set Shape Opacity ({})", vector_shape_name_or_kind(shape)),
);
}
}
}
pub fn set_vector_shape_bounds(
@@ -1472,6 +1593,14 @@ impl Engine {
x2: f32,
y2: f32,
) {
let layer_idx = self.document.layer_index_by_id(layer_id);
let before_shapes = self
.document
.get_layer_by_id(layer_id)
.and_then(|l| match &l.data {
LayerData::Vector { shapes } => Some(shapes.clone()),
_ => None,
});
if let Some(layer) = self.document.get_layer_by_id_mut(layer_id) {
if let LayerData::Vector { ref mut shapes } = layer.data {
if let Some(shape) = shapes.get_mut(shape_idx) {
@@ -1482,9 +1611,36 @@ impl Engine {
}
}
}
if let (Some(idx), Some(before)) = (layer_idx, before_shapes) {
if let Some(shape) = self
.document
.get_layer_by_id(layer_id)
.and_then(|l| match &l.data {
LayerData::Vector { shapes } => shapes.get(shape_idx),
_ => None,
})
{
self.document.push_vector_snapshot(
idx,
Some(before),
format!(
"Resize Vector Shape ({})",
vector_shape_name_or_kind(shape)
),
);
}
}
}
pub fn set_vector_shape_hardness(&mut self, layer_id: u64, shape_idx: usize, hardness: f32) {
let layer_idx = self.document.layer_index_by_id(layer_id);
let before_shapes = self
.document
.get_layer_by_id(layer_id)
.and_then(|l| match &l.data {
LayerData::Vector { shapes } => Some(shapes.clone()),
_ => None,
});
if let Some(layer) = self.document.get_layer_by_id_mut(layer_id) {
if let LayerData::Vector { ref mut shapes } = layer.data {
if let Some(shape) = shapes.get_mut(shape_idx) {
@@ -1495,6 +1651,25 @@ impl Engine {
}
}
}
if let (Some(idx), Some(before)) = (layer_idx, before_shapes) {
if let Some(shape) = self
.document
.get_layer_by_id(layer_id)
.and_then(|l| match &l.data {
LayerData::Vector { shapes } => shapes.get(shape_idx),
_ => None,
})
{
self.document.push_vector_snapshot(
idx,
Some(before),
format!(
"Set Shape Hardness ({})",
vector_shape_name_or_kind(shape)
),
);
}
}
}
pub fn set_vector_shape_line_caps(
@@ -1575,6 +1750,14 @@ impl Engine {
}
pub fn set_vector_shape_angle(&mut self, layer_id: u64, shape_idx: usize, angle: f32) {
let layer_idx = self.document.layer_index_by_id(layer_id);
let before_shapes = self
.document
.get_layer_by_id(layer_id)
.and_then(|l| match &l.data {
LayerData::Vector { shapes } => Some(shapes.clone()),
_ => None,
});
if let Some(layer) = self.document.get_layer_by_id_mut(layer_id) {
if let LayerData::Vector { ref mut shapes } = layer.data {
if let Some(shape) = shapes.get_mut(shape_idx) {
@@ -1585,6 +1768,25 @@ impl Engine {
}
}
}
if let (Some(idx), Some(before)) = (layer_idx, before_shapes) {
if let Some(shape) = self
.document
.get_layer_by_id(layer_id)
.and_then(|l| match &l.data {
LayerData::Vector { shapes } => shapes.get(shape_idx),
_ => None,
})
{
self.document.push_vector_snapshot(
idx,
Some(before),
format!(
"Rotate Vector Shape ({})",
vector_shape_name_or_kind(shape)
),
);
}
}
}
pub fn set_vector_shape_svg(&mut self, layer_id: u64, shape_idx: usize, new_svg: &str) {
@@ -1619,6 +1821,14 @@ impl Engine {
}
pub fn reorder_vector_shape(&mut self, layer_id: u64, from_idx: usize, to_idx: usize) {
let layer_idx = self.document.layer_index_by_id(layer_id);
let before_shapes = self
.document
.get_layer_by_id(layer_id)
.and_then(|l| match &l.data {
LayerData::Vector { shapes } => Some(shapes.clone()),
_ => None,
});
if let Some(layer) = self.document.get_layer_by_id_mut(layer_id) {
if let LayerData::Vector { ref mut shapes } = layer.data {
if from_idx < shapes.len() && to_idx < shapes.len() {
@@ -1630,9 +1840,27 @@ impl Engine {
}
}
}
if let (Some(idx), Some(before)) = (layer_idx, before_shapes) {
self.document.push_vector_snapshot(
idx,
Some(before),
"Reorder Vector Shape".to_string(),
);
}
}
pub fn delete_vector_shape(&mut self, layer_id: u64, shape_idx: usize) {
let layer_idx = self.document.layer_index_by_id(layer_id);
let before_shapes = self
.document
.get_layer_by_id(layer_id)
.and_then(|l| match &l.data {
LayerData::Vector { shapes } => Some(shapes.clone()),
_ => None,
});
let removed_kind = before_shapes.as_ref().and_then(|shapes| {
shapes.get(shape_idx).map(vector_shape_name_or_kind)
});
if let Some(layer) = self.document.get_layer_by_id_mut(layer_id) {
if let LayerData::Vector { ref mut shapes } = layer.data {
if shape_idx < shapes.len() {
@@ -1643,6 +1871,13 @@ impl Engine {
}
}
}
if let (Some(idx), Some(before)) = (layer_idx, before_shapes) {
let desc = removed_kind.map_or_else(
|| "Delete Vector Shape".to_string(),
|kind| format!("Delete Vector Shape ({})", kind),
);
self.document.push_vector_snapshot(idx, Some(before), desc);
}
}
pub fn boolean_vector_shapes(
@@ -3996,6 +4231,57 @@ impl Engine {
}
}
/// Returns a short label for the shape kind, used in history descriptions.
pub fn vector_shape_kind(shape: &VectorShape) -> String {
match shape {
VectorShape::Line { .. } => "Line".to_string(),
VectorShape::Rect { .. } => "Rect".to_string(),
VectorShape::Circle { .. } => "Circle".to_string(),
VectorShape::Arrow { .. } => "Arrow".to_string(),
VectorShape::Star { .. } => "Star".to_string(),
VectorShape::Polygon { .. } => "Polygon".to_string(),
VectorShape::Rhombus { .. } => "Rhombus".to_string(),
VectorShape::Cylinder { .. } => "Cylinder".to_string(),
VectorShape::Heart { .. } => "Heart".to_string(),
VectorShape::Bubble { .. } => "Bubble".to_string(),
VectorShape::Gear { .. } => "Gear".to_string(),
VectorShape::Cross { .. } => "Cross".to_string(),
VectorShape::Crescent { .. } => "Crescent".to_string(),
VectorShape::Bolt { .. } => "Bolt".to_string(),
VectorShape::Arrow4 { .. } => "Arrow4".to_string(),
VectorShape::SvgShape { kind, .. } => kind.clone(),
VectorShape::FreePath { .. } => "Path".to_string(),
}
}
/// Returns the shape's name if non-empty, otherwise its kind label.
pub fn vector_shape_name_or_kind(shape: &VectorShape) -> String {
let name = match shape {
VectorShape::Line { name, .. }
| VectorShape::Rect { name, .. }
| VectorShape::Circle { name, .. }
| VectorShape::Arrow { name, .. }
| VectorShape::Star { name, .. }
| VectorShape::Polygon { name, .. }
| VectorShape::Rhombus { name, .. }
| VectorShape::Cylinder { name, .. }
| VectorShape::Heart { name, .. }
| VectorShape::Bubble { name, .. }
| VectorShape::Gear { name, .. }
| VectorShape::Cross { name, .. }
| VectorShape::Crescent { name, .. }
| VectorShape::Bolt { name, .. }
| VectorShape::Arrow4 { name, .. }
| VectorShape::SvgShape { name, .. }
| VectorShape::FreePath { name, .. } => name.as_str(),
};
if name.is_empty() {
vector_shape_kind(shape).to_string()
} else {
name.to_string()
}
}
fn parse_hex_color(hex: &str) -> [u8; 4] {
let h = hex.trim_start_matches('#');
if h.len() >= 6 {