Can set stroke and fill on text and shapes (#551)
* Can set stroke and fill on text and shapes * resend layout on failed update * text input properly resets on bad input * support modifying gradients * can modify gradients in the properties panel * updated labels * remove heap allocation in favor of RC * removed redundent line * oops
This commit is contained in:
parent
93dffb8741
commit
a73d9b5811
|
|
@ -54,5 +54,5 @@ pub const FILE_EXPORT_SUFFIX: &str = ".svg";
|
||||||
pub const COLOR_ACCENT: Color = Color::from_unsafe(0x00 as f32 / 255., 0xA8 as f32 / 255., 0xFF as f32 / 255.);
|
pub const COLOR_ACCENT: Color = Color::from_unsafe(0x00 as f32 / 255., 0xA8 as f32 / 255., 0xFF as f32 / 255.);
|
||||||
|
|
||||||
// Document
|
// Document
|
||||||
pub const GRAPHITE_DOCUMENT_VERSION: &str = "0.0.3";
|
pub const GRAPHITE_DOCUMENT_VERSION: &str = "0.0.4";
|
||||||
pub const VIEWPORT_ZOOM_TO_FIT_PADDING_SCALE_FACTOR: f32 = 1.05;
|
pub const VIEWPORT_ZOOM_TO_FIT_PADDING_SCALE_FACTOR: f32 = 1.05;
|
||||||
|
|
|
||||||
|
|
@ -480,7 +480,7 @@ impl PropertyHolder for DocumentMessageHandler {
|
||||||
checked: self.snapping_enabled,
|
checked: self.snapping_enabled,
|
||||||
icon: "Snapping".into(),
|
icon: "Snapping".into(),
|
||||||
tooltip: "Snapping".into(),
|
tooltip: "Snapping".into(),
|
||||||
on_update: WidgetCallback::new(|updated_optional_input| DocumentMessage::SetSnapping { snap: updated_optional_input.checked }.into()),
|
on_update: WidgetCallback::new(|optional_input: &OptionalInput| DocumentMessage::SetSnapping { snap: optional_input.checked }.into()),
|
||||||
})),
|
})),
|
||||||
WidgetHolder::new(Widget::PopoverButton(PopoverButton {
|
WidgetHolder::new(Widget::PopoverButton(PopoverButton {
|
||||||
title: "Snapping".into(),
|
title: "Snapping".into(),
|
||||||
|
|
@ -508,12 +508,7 @@ impl PropertyHolder for DocumentMessageHandler {
|
||||||
checked: self.overlays_visible,
|
checked: self.overlays_visible,
|
||||||
icon: "Overlays".into(),
|
icon: "Overlays".into(),
|
||||||
tooltip: "Overlays".into(),
|
tooltip: "Overlays".into(),
|
||||||
on_update: WidgetCallback::new(|updated_optional_input| {
|
on_update: WidgetCallback::new(|optional_input: &OptionalInput| DocumentMessage::SetOverlaysVisibility { visible: optional_input.checked }.into()),
|
||||||
DocumentMessage::SetOverlaysVisibility {
|
|
||||||
visible: updated_optional_input.checked,
|
|
||||||
}
|
|
||||||
.into()
|
|
||||||
}),
|
|
||||||
})),
|
})),
|
||||||
WidgetHolder::new(Widget::PopoverButton(PopoverButton {
|
WidgetHolder::new(Widget::PopoverButton(PopoverButton {
|
||||||
title: "Overlays".into(),
|
title: "Overlays".into(),
|
||||||
|
|
@ -561,7 +556,7 @@ impl PropertyHolder for DocumentMessageHandler {
|
||||||
unit: "°".into(),
|
unit: "°".into(),
|
||||||
value: self.movement_handler.tilt / (std::f64::consts::PI / 180.),
|
value: self.movement_handler.tilt / (std::f64::consts::PI / 180.),
|
||||||
increment_factor: 15.,
|
increment_factor: 15.,
|
||||||
on_update: WidgetCallback::new(|number_input| {
|
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
||||||
MovementMessage::SetCanvasRotation {
|
MovementMessage::SetCanvasRotation {
|
||||||
angle_radians: number_input.value * (std::f64::consts::PI / 180.),
|
angle_radians: number_input.value * (std::f64::consts::PI / 180.),
|
||||||
}
|
}
|
||||||
|
|
@ -603,7 +598,7 @@ impl PropertyHolder for DocumentMessageHandler {
|
||||||
value: self.movement_handler.zoom * 100.,
|
value: self.movement_handler.zoom * 100.,
|
||||||
min: Some(0.000001),
|
min: Some(0.000001),
|
||||||
max: Some(1000000.),
|
max: Some(1000000.),
|
||||||
on_update: WidgetCallback::new(|number_input| {
|
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
||||||
MovementMessage::SetCanvasZoom {
|
MovementMessage::SetCanvasZoom {
|
||||||
zoom_factor: number_input.value / 100.,
|
zoom_factor: number_input.value / 100.,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::message_prelude::*;
|
use crate::message_prelude::*;
|
||||||
|
|
||||||
|
use graphene::layers::style::Fill;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[remain::sorted]
|
#[remain::sorted]
|
||||||
|
|
@ -9,8 +10,11 @@ pub enum PropertiesPanelMessage {
|
||||||
CheckSelectedWasDeleted { path: Vec<LayerId> },
|
CheckSelectedWasDeleted { path: Vec<LayerId> },
|
||||||
CheckSelectedWasUpdated { path: Vec<LayerId> },
|
CheckSelectedWasUpdated { path: Vec<LayerId> },
|
||||||
ClearSelection,
|
ClearSelection,
|
||||||
|
ModifyFill { fill: Fill },
|
||||||
ModifyName { name: String },
|
ModifyName { name: String },
|
||||||
|
ModifyStroke { color: String, weight: f64 },
|
||||||
ModifyTransform { value: f64, transform_op: TransformOp },
|
ModifyTransform { value: f64, transform_op: TransformOp },
|
||||||
|
ResendActiveProperties,
|
||||||
SetActiveLayers { paths: Vec<Vec<LayerId>> },
|
SetActiveLayers { paths: Vec<Vec<LayerId>> },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,16 @@ use crate::layout::widgets::{
|
||||||
};
|
};
|
||||||
use crate::message_prelude::*;
|
use crate::message_prelude::*;
|
||||||
|
|
||||||
|
use graphene::color::Color;
|
||||||
use graphene::document::Document as GrapheneDocument;
|
use graphene::document::Document as GrapheneDocument;
|
||||||
use graphene::layers::layer_info::{Layer, LayerDataType};
|
use graphene::layers::layer_info::{Layer, LayerDataType};
|
||||||
|
use graphene::layers::style::{Fill, Stroke};
|
||||||
use graphene::{LayerId, Operation};
|
use graphene::{LayerId, Operation};
|
||||||
|
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::f64::consts::PI;
|
use std::f64::consts::PI;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
trait DAffine2Utils {
|
trait DAffine2Utils {
|
||||||
fn width(&self) -> f64;
|
fn width(&self) -> f64;
|
||||||
|
|
@ -152,6 +155,21 @@ impl MessageHandler<PropertiesPanelMessage, &GrapheneDocument> for PropertiesPan
|
||||||
let path = self.active_path.clone().expect("Received update for properties panel with no active layer");
|
let path = self.active_path.clone().expect("Received update for properties panel with no active layer");
|
||||||
responses.push_back(DocumentMessage::SetLayerName { layer_path: path, name }.into())
|
responses.push_back(DocumentMessage::SetLayerName { layer_path: path, name }.into())
|
||||||
}
|
}
|
||||||
|
ModifyFill { fill } => {
|
||||||
|
let path = self.active_path.clone().expect("Received update for properties panel with no active layer");
|
||||||
|
responses.push_back(Operation::SetLayerFill { path, fill }.into());
|
||||||
|
}
|
||||||
|
ModifyStroke { color, weight } => {
|
||||||
|
let path = self.active_path.clone().expect("Received update for properties panel with no active layer");
|
||||||
|
let layer = graphene_document.layer(&path).unwrap();
|
||||||
|
if let Some(color) = Color::from_rgba_str(&color).or(Color::from_rgb_str(&color)) {
|
||||||
|
let stroke = Stroke::new(color, weight as f32);
|
||||||
|
responses.push_back(Operation::SetLayerStroke { path, stroke }.into())
|
||||||
|
} else {
|
||||||
|
// Failed to update, Show user unchanged state
|
||||||
|
register_layer_properties(layer, responses)
|
||||||
|
}
|
||||||
|
}
|
||||||
CheckSelectedWasUpdated { path } => {
|
CheckSelectedWasUpdated { path } => {
|
||||||
if self.matches_selected(&path) {
|
if self.matches_selected(&path) {
|
||||||
let layer = graphene_document.layer(&path).unwrap();
|
let layer = graphene_document.layer(&path).unwrap();
|
||||||
|
|
@ -177,6 +195,11 @@ impl MessageHandler<PropertiesPanelMessage, &GrapheneDocument> for PropertiesPan
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ResendActiveProperties => {
|
||||||
|
let path = self.active_path.clone().expect("Received update for properties panel with no active layer");
|
||||||
|
let layer = graphene_document.layer(&path).unwrap();
|
||||||
|
register_layer_properties(layer, responses)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -217,7 +240,7 @@ fn register_layer_properties(layer: &Layer, responses: &mut VecDeque<Message>) {
|
||||||
})),
|
})),
|
||||||
WidgetHolder::new(Widget::TextInput(TextInput {
|
WidgetHolder::new(Widget::TextInput(TextInput {
|
||||||
value: layer.name.clone().unwrap_or_else(|| "Untitled".to_string()),
|
value: layer.name.clone().unwrap_or_else(|| "Untitled".to_string()),
|
||||||
on_update: WidgetCallback::new(|text_input| PropertiesPanelMessage::ModifyName { name: text_input.value.clone() }.into()),
|
on_update: WidgetCallback::new(|text_input: &TextInput| PropertiesPanelMessage::ModifyName { name: text_input.value.clone() }.into()),
|
||||||
})),
|
})),
|
||||||
WidgetHolder::new(Widget::Separator(Separator {
|
WidgetHolder::new(Widget::Separator(Separator {
|
||||||
separator_type: SeparatorType::Related,
|
separator_type: SeparatorType::Related,
|
||||||
|
|
@ -232,13 +255,21 @@ fn register_layer_properties(layer: &Layer, responses: &mut VecDeque<Message>) {
|
||||||
|
|
||||||
let properties_body = match &layer.data {
|
let properties_body = match &layer.data {
|
||||||
LayerDataType::Folder(_) => {
|
LayerDataType::Folder(_) => {
|
||||||
vec![node_section_transform(layer)]
|
vec![]
|
||||||
}
|
}
|
||||||
LayerDataType::Shape(_) => {
|
LayerDataType::Shape(shape) => {
|
||||||
vec![node_section_transform(layer)]
|
vec![
|
||||||
|
node_section_transform(layer),
|
||||||
|
node_section_fill(&shape.style.fill()),
|
||||||
|
node_section_stroke(&shape.style.stroke().unwrap_or_default()),
|
||||||
|
]
|
||||||
}
|
}
|
||||||
LayerDataType::Text(_) => {
|
LayerDataType::Text(text) => {
|
||||||
vec![node_section_transform(layer)]
|
vec![
|
||||||
|
node_section_transform(layer),
|
||||||
|
node_section_fill(&text.style.fill()),
|
||||||
|
node_section_stroke(&text.style.stroke().unwrap_or_default()),
|
||||||
|
]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -277,7 +308,7 @@ fn node_section_transform(layer: &Layer) -> LayoutRow {
|
||||||
value: layer.transform.x(),
|
value: layer.transform.x(),
|
||||||
label: "X".into(),
|
label: "X".into(),
|
||||||
unit: " px".into(),
|
unit: " px".into(),
|
||||||
on_update: WidgetCallback::new(|number_input| {
|
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
||||||
PropertiesPanelMessage::ModifyTransform {
|
PropertiesPanelMessage::ModifyTransform {
|
||||||
value: number_input.value,
|
value: number_input.value,
|
||||||
transform_op: TransformOp::X,
|
transform_op: TransformOp::X,
|
||||||
|
|
@ -294,7 +325,7 @@ fn node_section_transform(layer: &Layer) -> LayoutRow {
|
||||||
value: layer.transform.y(),
|
value: layer.transform.y(),
|
||||||
label: "Y".into(),
|
label: "Y".into(),
|
||||||
unit: " px".into(),
|
unit: " px".into(),
|
||||||
on_update: WidgetCallback::new(|number_input| {
|
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
||||||
PropertiesPanelMessage::ModifyTransform {
|
PropertiesPanelMessage::ModifyTransform {
|
||||||
value: number_input.value,
|
value: number_input.value,
|
||||||
transform_op: TransformOp::Y,
|
transform_op: TransformOp::Y,
|
||||||
|
|
@ -320,7 +351,7 @@ fn node_section_transform(layer: &Layer) -> LayoutRow {
|
||||||
value: layer.transform.width(),
|
value: layer.transform.width(),
|
||||||
label: "W".into(),
|
label: "W".into(),
|
||||||
unit: " px".into(),
|
unit: " px".into(),
|
||||||
on_update: WidgetCallback::new(|number_input| {
|
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
||||||
PropertiesPanelMessage::ModifyTransform {
|
PropertiesPanelMessage::ModifyTransform {
|
||||||
value: number_input.value,
|
value: number_input.value,
|
||||||
transform_op: TransformOp::Width,
|
transform_op: TransformOp::Width,
|
||||||
|
|
@ -337,7 +368,7 @@ fn node_section_transform(layer: &Layer) -> LayoutRow {
|
||||||
value: layer.transform.height(),
|
value: layer.transform.height(),
|
||||||
label: "H".into(),
|
label: "H".into(),
|
||||||
unit: " px".into(),
|
unit: " px".into(),
|
||||||
on_update: WidgetCallback::new(|number_input| {
|
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
||||||
PropertiesPanelMessage::ModifyTransform {
|
PropertiesPanelMessage::ModifyTransform {
|
||||||
value: number_input.value,
|
value: number_input.value,
|
||||||
transform_op: TransformOp::Height,
|
transform_op: TransformOp::Height,
|
||||||
|
|
@ -363,7 +394,7 @@ fn node_section_transform(layer: &Layer) -> LayoutRow {
|
||||||
value: layer.transform.rotation() * 180. / PI,
|
value: layer.transform.rotation() * 180. / PI,
|
||||||
label: "R".into(),
|
label: "R".into(),
|
||||||
unit: "°".into(),
|
unit: "°".into(),
|
||||||
on_update: WidgetCallback::new(|number_input| {
|
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
||||||
PropertiesPanelMessage::ModifyTransform {
|
PropertiesPanelMessage::ModifyTransform {
|
||||||
value: number_input.value / 180. * PI,
|
value: number_input.value / 180. * PI,
|
||||||
transform_op: TransformOp::Rotation,
|
transform_op: TransformOp::Rotation,
|
||||||
|
|
@ -377,3 +408,161 @@ fn node_section_transform(layer: &Layer) -> LayoutRow {
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn node_section_fill(fill: &Fill) -> LayoutRow {
|
||||||
|
match fill {
|
||||||
|
Fill::Solid(color) => LayoutRow::Section {
|
||||||
|
name: "Fill".into(),
|
||||||
|
layout: vec![LayoutRow::Row {
|
||||||
|
name: "".into(),
|
||||||
|
widgets: vec![
|
||||||
|
WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||||
|
value: "Color".into(),
|
||||||
|
..TextLabel::default()
|
||||||
|
})),
|
||||||
|
WidgetHolder::new(Widget::Separator(Separator {
|
||||||
|
separator_type: SeparatorType::Related,
|
||||||
|
direction: SeparatorDirection::Horizontal,
|
||||||
|
})),
|
||||||
|
WidgetHolder::new(Widget::TextInput(TextInput {
|
||||||
|
value: color.rgba_hex(),
|
||||||
|
on_update: WidgetCallback::new(|text_input: &TextInput| {
|
||||||
|
if let Some(color) = Color::from_rgba_str(&text_input.value).or(Color::from_rgb_str(&text_input.value)) {
|
||||||
|
let new_fill = Fill::Solid(color);
|
||||||
|
PropertiesPanelMessage::ModifyFill { fill: new_fill }.into()
|
||||||
|
} else {
|
||||||
|
PropertiesPanelMessage::ResendActiveProperties.into()
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
Fill::LinearGradient(gradient) => {
|
||||||
|
let gradient_1 = Rc::new(gradient.clone());
|
||||||
|
let gradient_2 = gradient_1.clone();
|
||||||
|
LayoutRow::Section {
|
||||||
|
name: "Fill".into(),
|
||||||
|
layout: vec![
|
||||||
|
LayoutRow::Row {
|
||||||
|
name: "".into(),
|
||||||
|
widgets: vec![
|
||||||
|
WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||||
|
value: "Gradient: 0%".into(),
|
||||||
|
..TextLabel::default()
|
||||||
|
})),
|
||||||
|
WidgetHolder::new(Widget::Separator(Separator {
|
||||||
|
separator_type: SeparatorType::Related,
|
||||||
|
direction: SeparatorDirection::Horizontal,
|
||||||
|
})),
|
||||||
|
WidgetHolder::new(Widget::TextInput(TextInput {
|
||||||
|
value: gradient_1.positions[0].1.rgba_hex(),
|
||||||
|
on_update: WidgetCallback::new(move |text_input: &TextInput| {
|
||||||
|
if let Some(color) = Color::from_rgba_str(&text_input.value).or(Color::from_rgb_str(&text_input.value)) {
|
||||||
|
let mut new_gradient = (*gradient_1).clone();
|
||||||
|
new_gradient.positions[0].1 = color;
|
||||||
|
PropertiesPanelMessage::ModifyFill {
|
||||||
|
fill: Fill::LinearGradient(new_gradient),
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
PropertiesPanelMessage::ResendActiveProperties.into()
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
LayoutRow::Row {
|
||||||
|
name: "".into(),
|
||||||
|
widgets: vec![
|
||||||
|
WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||||
|
value: "Gradient: 100%".into(),
|
||||||
|
..TextLabel::default()
|
||||||
|
})),
|
||||||
|
WidgetHolder::new(Widget::Separator(Separator {
|
||||||
|
separator_type: SeparatorType::Related,
|
||||||
|
direction: SeparatorDirection::Horizontal,
|
||||||
|
})),
|
||||||
|
WidgetHolder::new(Widget::TextInput(TextInput {
|
||||||
|
value: gradient_2.positions[1].1.rgba_hex(),
|
||||||
|
on_update: WidgetCallback::new(move |text_input: &TextInput| {
|
||||||
|
if let Some(color) = Color::from_rgba_str(&text_input.value).or(Color::from_rgb_str(&text_input.value)) {
|
||||||
|
let mut new_gradient = (*gradient_2).clone();
|
||||||
|
new_gradient.positions[1].1 = color;
|
||||||
|
PropertiesPanelMessage::ModifyFill {
|
||||||
|
fill: Fill::LinearGradient(new_gradient),
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
PropertiesPanelMessage::ResendActiveProperties.into()
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Fill::None => panic!("`node_section_fill` called on a shape that does not have a fill"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn node_section_stroke(stroke: &Stroke) -> LayoutRow {
|
||||||
|
let color = stroke.color();
|
||||||
|
let weight = stroke.width();
|
||||||
|
LayoutRow::Section {
|
||||||
|
name: "Stroke".into(),
|
||||||
|
layout: vec![
|
||||||
|
LayoutRow::Row {
|
||||||
|
name: "".into(),
|
||||||
|
widgets: vec![
|
||||||
|
WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||||
|
value: "Color".into(),
|
||||||
|
..TextLabel::default()
|
||||||
|
})),
|
||||||
|
WidgetHolder::new(Widget::Separator(Separator {
|
||||||
|
separator_type: SeparatorType::Related,
|
||||||
|
direction: SeparatorDirection::Horizontal,
|
||||||
|
})),
|
||||||
|
WidgetHolder::new(Widget::TextInput(TextInput {
|
||||||
|
value: stroke.color().rgba_hex(),
|
||||||
|
on_update: WidgetCallback::new(move |text_input: &TextInput| {
|
||||||
|
PropertiesPanelMessage::ModifyStroke {
|
||||||
|
color: text_input.value.clone(),
|
||||||
|
weight: weight as f64,
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
LayoutRow::Row {
|
||||||
|
name: "".into(),
|
||||||
|
widgets: vec![
|
||||||
|
WidgetHolder::new(Widget::TextLabel(TextLabel {
|
||||||
|
value: "Weight".into(),
|
||||||
|
..TextLabel::default()
|
||||||
|
})),
|
||||||
|
WidgetHolder::new(Widget::Separator(Separator {
|
||||||
|
separator_type: SeparatorType::Related,
|
||||||
|
direction: SeparatorDirection::Horizontal,
|
||||||
|
})),
|
||||||
|
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||||
|
value: stroke.width() as f64,
|
||||||
|
is_integer: true,
|
||||||
|
min: Some(0.),
|
||||||
|
unit: " px".into(),
|
||||||
|
on_update: WidgetCallback::new(move |number_input: &NumberInput| {
|
||||||
|
PropertiesPanelMessage::ModifyStroke {
|
||||||
|
color: color.rgba_hex(),
|
||||||
|
weight: number_input.value,
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}),
|
||||||
|
..NumberInput::default()
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
use super::layout_message::LayoutTarget;
|
use super::layout_message::LayoutTarget;
|
||||||
use crate::message_prelude::*;
|
use crate::message_prelude::*;
|
||||||
|
|
||||||
|
|
@ -130,18 +132,18 @@ impl WidgetHolder {
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct WidgetCallback<T> {
|
pub struct WidgetCallback<T> {
|
||||||
pub callback: fn(&T) -> Message,
|
pub callback: Rc<dyn Fn(&T) -> Message + 'static>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> WidgetCallback<T> {
|
impl<T> WidgetCallback<T> {
|
||||||
pub fn new(callback: fn(&T) -> Message) -> Self {
|
pub fn new(callback: impl Fn(&T) -> Message + 'static) -> Self {
|
||||||
Self { callback }
|
Self { callback: Rc::new(callback) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Default for WidgetCallback<T> {
|
impl<T> Default for WidgetCallback<T> {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self { callback: |_| Message::NoOp }
|
Self::new(|_| Message::NoOp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ impl PropertyHolder for FreehandTool {
|
||||||
value: self.options.line_weight as f64,
|
value: self.options.line_weight as f64,
|
||||||
is_integer: true,
|
is_integer: true,
|
||||||
min: Some(1.),
|
min: Some(1.),
|
||||||
on_update: WidgetCallback::new(|number_input| FreehandToolMessage::UpdateOptions(FreehandToolMessageOptionsUpdate::LineWeight(number_input.value as u32)).into()),
|
on_update: WidgetCallback::new(|number_input: &NumberInput| FreehandToolMessage::UpdateOptions(FreehandToolMessageOptionsUpdate::LineWeight(number_input.value as u32)).into()),
|
||||||
..NumberInput::default()
|
..NumberInput::default()
|
||||||
}))],
|
}))],
|
||||||
}])
|
}])
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ impl PropertyHolder for LineTool {
|
||||||
value: self.options.line_weight as f64,
|
value: self.options.line_weight as f64,
|
||||||
is_integer: true,
|
is_integer: true,
|
||||||
min: Some(0.),
|
min: Some(0.),
|
||||||
on_update: WidgetCallback::new(|number_input| LineToolMessage::UpdateOptions(LineOptionsUpdate::LineWeight(number_input.value as u32)).into()),
|
on_update: WidgetCallback::new(|number_input: &NumberInput| LineToolMessage::UpdateOptions(LineOptionsUpdate::LineWeight(number_input.value as u32)).into()),
|
||||||
..NumberInput::default()
|
..NumberInput::default()
|
||||||
}))],
|
}))],
|
||||||
}])
|
}])
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@ impl PropertyHolder for PenTool {
|
||||||
value: self.options.line_weight as f64,
|
value: self.options.line_weight as f64,
|
||||||
is_integer: true,
|
is_integer: true,
|
||||||
min: Some(0.),
|
min: Some(0.),
|
||||||
on_update: WidgetCallback::new(|number_input| PenToolMessage::UpdateOptions(PenOptionsUpdate::LineWeight(number_input.value as u32)).into()),
|
on_update: WidgetCallback::new(|number_input: &NumberInput| PenToolMessage::UpdateOptions(PenOptionsUpdate::LineWeight(number_input.value as u32)).into()),
|
||||||
..NumberInput::default()
|
..NumberInput::default()
|
||||||
}))],
|
}))],
|
||||||
}])
|
}])
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ impl PropertyHolder for ShapeTool {
|
||||||
is_integer: true,
|
is_integer: true,
|
||||||
min: Some(3.),
|
min: Some(3.),
|
||||||
max: Some(256.),
|
max: Some(256.),
|
||||||
on_update: WidgetCallback::new(|number_input| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Vertices(number_input.value as u8)).into()),
|
on_update: WidgetCallback::new(|number_input: &NumberInput| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Vertices(number_input.value as u8)).into()),
|
||||||
..NumberInput::default()
|
..NumberInput::default()
|
||||||
}))],
|
}))],
|
||||||
}])
|
}])
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ impl PropertyHolder for SplineTool {
|
||||||
value: self.options.line_weight as f64,
|
value: self.options.line_weight as f64,
|
||||||
is_integer: true,
|
is_integer: true,
|
||||||
min: Some(0.),
|
min: Some(0.),
|
||||||
on_update: WidgetCallback::new(|number_input| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::LineWeight(number_input.value as u32)).into()),
|
on_update: WidgetCallback::new(|number_input: &NumberInput| SplineToolMessage::UpdateOptions(SplineOptionsUpdate::LineWeight(number_input.value as u32)).into()),
|
||||||
..NumberInput::default()
|
..NumberInput::default()
|
||||||
}))],
|
}))],
|
||||||
}])
|
}])
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ impl PropertyHolder for TextTool {
|
||||||
value: self.options.font_size as f64,
|
value: self.options.font_size as f64,
|
||||||
is_integer: true,
|
is_integer: true,
|
||||||
min: Some(1.),
|
min: Some(1.),
|
||||||
on_update: WidgetCallback::new(|number_input| TextMessage::UpdateOptions(TextOptionsUpdate::FontSize(number_input.value as u32)).into()),
|
on_update: WidgetCallback::new(|number_input: &NumberInput| TextMessage::UpdateOptions(TextOptionsUpdate::FontSize(number_input.value as u32)).into()),
|
||||||
..NumberInput::default()
|
..NumberInput::default()
|
||||||
}))],
|
}))],
|
||||||
}])
|
}])
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.widget-section {
|
.widget-section {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
flex: 0 0 24px;
|
flex: 0 0 24px;
|
||||||
background: var(--color-4-dimgray);
|
background: var(--color-4-dimgray);
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,9 @@ export default defineComponent({
|
||||||
// TODO: Find a less hacky way to do this
|
// TODO: Find a less hacky way to do this
|
||||||
const inputElement = (this.$refs.fieldInput as typeof FieldInput).$refs.input as HTMLInputElement;
|
const inputElement = (this.$refs.fieldInput as typeof FieldInput).$refs.input as HTMLInputElement;
|
||||||
this.$emit("commitText", inputElement.value);
|
this.$emit("commitText", inputElement.value);
|
||||||
|
|
||||||
|
// Required if value is not changed by the parent component upon update:value event
|
||||||
|
inputElement.value = this.value;
|
||||||
},
|
},
|
||||||
onCancelTextChange() {
|
onCancelTextChange() {
|
||||||
this.editing = false;
|
this.editing = false;
|
||||||
|
|
|
||||||
|
|
@ -162,4 +162,41 @@ impl Color {
|
||||||
pub fn rgb_hex(&self) -> String {
|
pub fn rgb_hex(&self) -> String {
|
||||||
format!("{:02X?}{:02X?}{:02X?}", (self.r() * 255.) as u8, (self.g() * 255.) as u8, (self.b() * 255.) as u8,)
|
format!("{:02X?}{:02X?}{:02X?}", (self.r() * 255.) as u8, (self.g() * 255.) as u8, (self.b() * 255.) as u8,)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a color from a 8-character RGBA hex string (without a # prefix).
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```
|
||||||
|
/// use graphite_graphene::color::Color;
|
||||||
|
/// let color = Color::from_rgba_str("7C67FA61").unwrap();
|
||||||
|
/// assert!("7C67FA61" == color.rgba_hex())
|
||||||
|
/// ```
|
||||||
|
pub fn from_rgba_str(color_str: &str) -> Option<Color> {
|
||||||
|
if color_str.len() != 8 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let r = u8::from_str_radix(&color_str[0..2], 16).ok()?;
|
||||||
|
let g = u8::from_str_radix(&color_str[2..4], 16).ok()?;
|
||||||
|
let b = u8::from_str_radix(&color_str[4..6], 16).ok()?;
|
||||||
|
let a = u8::from_str_radix(&color_str[6..8], 16).ok()?;
|
||||||
|
|
||||||
|
Some(Color::from_rgba8(r, g, b, a))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a color from a 6-character RGB hex string (without a # prefix).
|
||||||
|
/// ```
|
||||||
|
/// use graphite_graphene::color::Color;
|
||||||
|
/// let color = Color::from_rgb_str("7C67FA").unwrap();
|
||||||
|
/// assert!("7C67FA" == color.rgb_hex())
|
||||||
|
/// ```
|
||||||
|
pub fn from_rgb_str(color_str: &str) -> Option<Color> {
|
||||||
|
if color_str.len() != 6 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let r = u8::from_str_radix(&color_str[0..2], 16).ok()?;
|
||||||
|
let g = u8::from_str_radix(&color_str[2..4], 16).ok()?;
|
||||||
|
let b = u8::from_str_radix(&color_str[4..6], 16).ok()?;
|
||||||
|
|
||||||
|
Some(Color::from_rgb8(r, g, b))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -775,11 +775,18 @@ impl Document {
|
||||||
let layer = self.layer_mut(&path)?;
|
let layer = self.layer_mut(&path)?;
|
||||||
match &mut layer.data {
|
match &mut layer.data {
|
||||||
LayerDataType::Shape(s) => s.style = style,
|
LayerDataType::Shape(s) => s.style = style,
|
||||||
|
LayerDataType::Text(text) => text.style = style,
|
||||||
_ => return Err(DocumentError::NotAShape),
|
_ => return Err(DocumentError::NotAShape),
|
||||||
}
|
}
|
||||||
self.mark_as_dirty(&path)?;
|
self.mark_as_dirty(&path)?;
|
||||||
Some([vec![DocumentChanged, LayerChanged { path: path.clone() }], update_thumbnails_upstream(&path)].concat())
|
Some([vec![DocumentChanged, LayerChanged { path: path.clone() }], update_thumbnails_upstream(&path)].concat())
|
||||||
}
|
}
|
||||||
|
Operation::SetLayerStroke { path, stroke } => {
|
||||||
|
let layer = self.layer_mut(&path)?;
|
||||||
|
layer.style_mut()?.set_stroke(stroke);
|
||||||
|
self.mark_as_dirty(&path)?;
|
||||||
|
Some([vec![DocumentChanged], update_thumbnails_upstream(&path)].concat())
|
||||||
|
}
|
||||||
Operation::SetLayerFill { path, fill } => {
|
Operation::SetLayerFill { path, fill } => {
|
||||||
let layer = self.layer_mut(&path)?;
|
let layer = self.layer_mut(&path)?;
|
||||||
layer.style_mut()?.set_fill(fill);
|
layer.style_mut()?.set_fill(fill);
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,7 @@ impl Fill {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct Stroke {
|
pub struct Stroke {
|
||||||
color: Color,
|
color: Color,
|
||||||
width: f32,
|
width: f32,
|
||||||
|
|
@ -157,6 +157,16 @@ impl Stroke {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Having an alpha of 1 to start with leads to a better experience with the properties panel
|
||||||
|
impl Default for Stroke {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
width: 0.,
|
||||||
|
color: Color::from_rgba8(0, 0, 0, 255),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
|
||||||
pub struct PathStyle {
|
pub struct PathStyle {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::boolean_ops::BooleanOperation as BooleanOperationType;
|
use crate::boolean_ops::BooleanOperation as BooleanOperationType;
|
||||||
use crate::layers::blend_mode::BlendMode;
|
use crate::layers::blend_mode::BlendMode;
|
||||||
use crate::layers::layer_info::Layer;
|
use crate::layers::layer_info::Layer;
|
||||||
use crate::layers::style;
|
use crate::layers::style::{self, Stroke};
|
||||||
use crate::LayerId;
|
use crate::LayerId;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
@ -180,6 +180,10 @@ pub enum Operation {
|
||||||
path: Vec<LayerId>,
|
path: Vec<LayerId>,
|
||||||
fill: style::Fill,
|
fill: style::Fill,
|
||||||
},
|
},
|
||||||
|
SetLayerStroke {
|
||||||
|
path: Vec<LayerId>,
|
||||||
|
stroke: Stroke,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Operation {
|
impl Operation {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue