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.);
|
||||
|
||||
// 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;
|
||||
|
|
|
|||
|
|
@ -480,7 +480,7 @@ impl PropertyHolder for DocumentMessageHandler {
|
|||
checked: self.snapping_enabled,
|
||||
icon: "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 {
|
||||
title: "Snapping".into(),
|
||||
|
|
@ -508,12 +508,7 @@ impl PropertyHolder for DocumentMessageHandler {
|
|||
checked: self.overlays_visible,
|
||||
icon: "Overlays".into(),
|
||||
tooltip: "Overlays".into(),
|
||||
on_update: WidgetCallback::new(|updated_optional_input| {
|
||||
DocumentMessage::SetOverlaysVisibility {
|
||||
visible: updated_optional_input.checked,
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
on_update: WidgetCallback::new(|optional_input: &OptionalInput| DocumentMessage::SetOverlaysVisibility { visible: optional_input.checked }.into()),
|
||||
})),
|
||||
WidgetHolder::new(Widget::PopoverButton(PopoverButton {
|
||||
title: "Overlays".into(),
|
||||
|
|
@ -561,7 +556,7 @@ impl PropertyHolder for DocumentMessageHandler {
|
|||
unit: "°".into(),
|
||||
value: self.movement_handler.tilt / (std::f64::consts::PI / 180.),
|
||||
increment_factor: 15.,
|
||||
on_update: WidgetCallback::new(|number_input| {
|
||||
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
||||
MovementMessage::SetCanvasRotation {
|
||||
angle_radians: number_input.value * (std::f64::consts::PI / 180.),
|
||||
}
|
||||
|
|
@ -603,7 +598,7 @@ impl PropertyHolder for DocumentMessageHandler {
|
|||
value: self.movement_handler.zoom * 100.,
|
||||
min: Some(0.000001),
|
||||
max: Some(1000000.),
|
||||
on_update: WidgetCallback::new(|number_input| {
|
||||
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
||||
MovementMessage::SetCanvasZoom {
|
||||
zoom_factor: number_input.value / 100.,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
use crate::message_prelude::*;
|
||||
|
||||
use graphene::layers::style::Fill;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[remain::sorted]
|
||||
|
|
@ -9,8 +10,11 @@ pub enum PropertiesPanelMessage {
|
|||
CheckSelectedWasDeleted { path: Vec<LayerId> },
|
||||
CheckSelectedWasUpdated { path: Vec<LayerId> },
|
||||
ClearSelection,
|
||||
ModifyFill { fill: Fill },
|
||||
ModifyName { name: String },
|
||||
ModifyStroke { color: String, weight: f64 },
|
||||
ModifyTransform { value: f64, transform_op: TransformOp },
|
||||
ResendActiveProperties,
|
||||
SetActiveLayers { paths: Vec<Vec<LayerId>> },
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,13 +6,16 @@ use crate::layout::widgets::{
|
|||
};
|
||||
use crate::message_prelude::*;
|
||||
|
||||
use graphene::color::Color;
|
||||
use graphene::document::Document as GrapheneDocument;
|
||||
use graphene::layers::layer_info::{Layer, LayerDataType};
|
||||
use graphene::layers::style::{Fill, Stroke};
|
||||
use graphene::{LayerId, Operation};
|
||||
|
||||
use glam::{DAffine2, DVec2};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::f64::consts::PI;
|
||||
use std::rc::Rc;
|
||||
|
||||
trait DAffine2Utils {
|
||||
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");
|
||||
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 } => {
|
||||
if self.matches_selected(&path) {
|
||||
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 {
|
||||
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 {
|
||||
separator_type: SeparatorType::Related,
|
||||
|
|
@ -232,13 +255,21 @@ fn register_layer_properties(layer: &Layer, responses: &mut VecDeque<Message>) {
|
|||
|
||||
let properties_body = match &layer.data {
|
||||
LayerDataType::Folder(_) => {
|
||||
vec![node_section_transform(layer)]
|
||||
vec![]
|
||||
}
|
||||
LayerDataType::Shape(_) => {
|
||||
vec![node_section_transform(layer)]
|
||||
LayerDataType::Shape(shape) => {
|
||||
vec![
|
||||
node_section_transform(layer),
|
||||
node_section_fill(&shape.style.fill()),
|
||||
node_section_stroke(&shape.style.stroke().unwrap_or_default()),
|
||||
]
|
||||
}
|
||||
LayerDataType::Text(_) => {
|
||||
vec![node_section_transform(layer)]
|
||||
LayerDataType::Text(text) => {
|
||||
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(),
|
||||
label: "X".into(),
|
||||
unit: " px".into(),
|
||||
on_update: WidgetCallback::new(|number_input| {
|
||||
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
||||
PropertiesPanelMessage::ModifyTransform {
|
||||
value: number_input.value,
|
||||
transform_op: TransformOp::X,
|
||||
|
|
@ -294,7 +325,7 @@ fn node_section_transform(layer: &Layer) -> LayoutRow {
|
|||
value: layer.transform.y(),
|
||||
label: "Y".into(),
|
||||
unit: " px".into(),
|
||||
on_update: WidgetCallback::new(|number_input| {
|
||||
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
||||
PropertiesPanelMessage::ModifyTransform {
|
||||
value: number_input.value,
|
||||
transform_op: TransformOp::Y,
|
||||
|
|
@ -320,7 +351,7 @@ fn node_section_transform(layer: &Layer) -> LayoutRow {
|
|||
value: layer.transform.width(),
|
||||
label: "W".into(),
|
||||
unit: " px".into(),
|
||||
on_update: WidgetCallback::new(|number_input| {
|
||||
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
||||
PropertiesPanelMessage::ModifyTransform {
|
||||
value: number_input.value,
|
||||
transform_op: TransformOp::Width,
|
||||
|
|
@ -337,7 +368,7 @@ fn node_section_transform(layer: &Layer) -> LayoutRow {
|
|||
value: layer.transform.height(),
|
||||
label: "H".into(),
|
||||
unit: " px".into(),
|
||||
on_update: WidgetCallback::new(|number_input| {
|
||||
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
||||
PropertiesPanelMessage::ModifyTransform {
|
||||
value: number_input.value,
|
||||
transform_op: TransformOp::Height,
|
||||
|
|
@ -363,7 +394,7 @@ fn node_section_transform(layer: &Layer) -> LayoutRow {
|
|||
value: layer.transform.rotation() * 180. / PI,
|
||||
label: "R".into(),
|
||||
unit: "°".into(),
|
||||
on_update: WidgetCallback::new(|number_input| {
|
||||
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
||||
PropertiesPanelMessage::ModifyTransform {
|
||||
value: number_input.value / 180. * PI,
|
||||
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 crate::message_prelude::*;
|
||||
|
||||
|
|
@ -130,18 +132,18 @@ impl WidgetHolder {
|
|||
|
||||
#[derive(Clone)]
|
||||
pub struct WidgetCallback<T> {
|
||||
pub callback: fn(&T) -> Message,
|
||||
pub callback: Rc<dyn Fn(&T) -> Message + 'static>,
|
||||
}
|
||||
|
||||
impl<T> WidgetCallback<T> {
|
||||
pub fn new(callback: fn(&T) -> Message) -> Self {
|
||||
Self { callback }
|
||||
pub fn new(callback: impl Fn(&T) -> Message + 'static) -> Self {
|
||||
Self { callback: Rc::new(callback) }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Default for WidgetCallback<T> {
|
||||
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,
|
||||
is_integer: true,
|
||||
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()
|
||||
}))],
|
||||
}])
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ impl PropertyHolder for LineTool {
|
|||
value: self.options.line_weight as f64,
|
||||
is_integer: true,
|
||||
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()
|
||||
}))],
|
||||
}])
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ impl PropertyHolder for PenTool {
|
|||
value: self.options.line_weight as f64,
|
||||
is_integer: true,
|
||||
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()
|
||||
}))],
|
||||
}])
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ impl PropertyHolder for ShapeTool {
|
|||
is_integer: true,
|
||||
min: Some(3.),
|
||||
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()
|
||||
}))],
|
||||
}])
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ impl PropertyHolder for SplineTool {
|
|||
value: self.options.line_weight as f64,
|
||||
is_integer: true,
|
||||
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()
|
||||
}))],
|
||||
}])
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ impl PropertyHolder for TextTool {
|
|||
value: self.options.font_size as f64,
|
||||
is_integer: true,
|
||||
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()
|
||||
}))],
|
||||
}])
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@
|
|||
|
||||
<style lang="scss">
|
||||
.widget-section {
|
||||
flex: 0 0 auto;
|
||||
|
||||
.header {
|
||||
flex: 0 0 24px;
|
||||
background: var(--color-4-dimgray);
|
||||
|
|
|
|||
|
|
@ -67,6 +67,9 @@ export default defineComponent({
|
|||
// TODO: Find a less hacky way to do this
|
||||
const inputElement = (this.$refs.fieldInput as typeof FieldInput).$refs.input as HTMLInputElement;
|
||||
this.$emit("commitText", inputElement.value);
|
||||
|
||||
// Required if value is not changed by the parent component upon update:value event
|
||||
inputElement.value = this.value;
|
||||
},
|
||||
onCancelTextChange() {
|
||||
this.editing = false;
|
||||
|
|
|
|||
|
|
@ -162,4 +162,41 @@ impl Color {
|
|||
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,)
|
||||
}
|
||||
|
||||
/// 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)?;
|
||||
match &mut layer.data {
|
||||
LayerDataType::Shape(s) => s.style = style,
|
||||
LayerDataType::Text(text) => text.style = style,
|
||||
_ => return Err(DocumentError::NotAShape),
|
||||
}
|
||||
self.mark_as_dirty(&path)?;
|
||||
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 } => {
|
||||
let layer = self.layer_mut(&path)?;
|
||||
layer.style_mut()?.set_fill(fill);
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ impl Fill {
|
|||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Stroke {
|
||||
color: Color,
|
||||
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)]
|
||||
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
|
||||
pub struct PathStyle {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use crate::boolean_ops::BooleanOperation as BooleanOperationType;
|
||||
use crate::layers::blend_mode::BlendMode;
|
||||
use crate::layers::layer_info::Layer;
|
||||
use crate::layers::style;
|
||||
use crate::layers::style::{self, Stroke};
|
||||
use crate::LayerId;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
|
@ -180,6 +180,10 @@ pub enum Operation {
|
|||
path: Vec<LayerId>,
|
||||
fill: style::Fill,
|
||||
},
|
||||
SetLayerStroke {
|
||||
path: Vec<LayerId>,
|
||||
stroke: Stroke,
|
||||
},
|
||||
}
|
||||
|
||||
impl Operation {
|
||||
|
|
|
|||
Loading…
Reference in New Issue