mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-27 19:18:12 +08:00
All shapes now have a Fill in the properties panel; color inputs are now optional (#583)
* Add aditional stroke properties * Make the colour input optional * Fix fmt * Apply code review changes * Code review nitpicks * Fix recursion Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
committed by
Keavon Chambers
co-authored by
Keavon Chambers
parent
a76bded12f
commit
66a100ff52
@@ -461,7 +461,7 @@ fn node_section_transform(layer: &Layer) -> LayoutRow {
|
|||||||
|
|
||||||
fn node_section_fill(fill: &Fill) -> Option<LayoutRow> {
|
fn node_section_fill(fill: &Fill) -> Option<LayoutRow> {
|
||||||
match fill {
|
match fill {
|
||||||
Fill::Solid(color) => Some(LayoutRow::Section {
|
Fill::Solid(_) | Fill::None => Some(LayoutRow::Section {
|
||||||
name: "Fill".into(),
|
name: "Fill".into(),
|
||||||
layout: vec![LayoutRow::Row {
|
layout: vec![LayoutRow::Row {
|
||||||
name: "".into(),
|
name: "".into(),
|
||||||
@@ -475,13 +475,17 @@ fn node_section_fill(fill: &Fill) -> Option<LayoutRow> {
|
|||||||
direction: SeparatorDirection::Horizontal,
|
direction: SeparatorDirection::Horizontal,
|
||||||
})),
|
})),
|
||||||
WidgetHolder::new(Widget::ColorInput(ColorInput {
|
WidgetHolder::new(Widget::ColorInput(ColorInput {
|
||||||
value: color.rgba_hex(),
|
value: if let Fill::Solid(color) = fill { Some(color.rgba_hex()) } else { None },
|
||||||
on_update: WidgetCallback::new(|text_input: &ColorInput| {
|
on_update: WidgetCallback::new(|text_input: &ColorInput| {
|
||||||
if let Some(color) = Color::from_rgba_str(&text_input.value).or_else(|| Color::from_rgb_str(&text_input.value)) {
|
if let Some(value) = &text_input.value {
|
||||||
let new_fill = Fill::Solid(color);
|
if let Some(color) = Color::from_rgba_str(value).or_else(|| Color::from_rgb_str(value)) {
|
||||||
PropertiesPanelMessage::ModifyFill { fill: new_fill }.into()
|
let new_fill = Fill::Solid(color);
|
||||||
|
PropertiesPanelMessage::ModifyFill { fill: new_fill }.into()
|
||||||
|
} else {
|
||||||
|
PropertiesPanelMessage::ResendActiveProperties.into()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
PropertiesPanelMessage::ResendActiveProperties.into()
|
PropertiesPanelMessage::ModifyFill { fill: Fill::None }.into()
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
})),
|
})),
|
||||||
@@ -506,17 +510,26 @@ fn node_section_fill(fill: &Fill) -> Option<LayoutRow> {
|
|||||||
direction: SeparatorDirection::Horizontal,
|
direction: SeparatorDirection::Horizontal,
|
||||||
})),
|
})),
|
||||||
WidgetHolder::new(Widget::ColorInput(ColorInput {
|
WidgetHolder::new(Widget::ColorInput(ColorInput {
|
||||||
value: gradient_1.positions[0].1.rgba_hex(),
|
value: gradient_1.positions[0].1.map(|color| color.rgba_hex()),
|
||||||
on_update: WidgetCallback::new(move |text_input: &ColorInput| {
|
on_update: WidgetCallback::new(move |text_input: &ColorInput| {
|
||||||
if let Some(color) = Color::from_rgba_str(&text_input.value).or_else(|| Color::from_rgb_str(&text_input.value)) {
|
if let Some(value) = &text_input.value {
|
||||||
|
if let Some(color) = Color::from_rgba_str(value).or_else(|| Color::from_rgb_str(value)) {
|
||||||
|
let mut new_gradient = (*gradient_1).clone();
|
||||||
|
new_gradient.positions[0].1 = Some(color);
|
||||||
|
PropertiesPanelMessage::ModifyFill {
|
||||||
|
fill: Fill::LinearGradient(new_gradient),
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
PropertiesPanelMessage::ResendActiveProperties.into()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
let mut new_gradient = (*gradient_1).clone();
|
let mut new_gradient = (*gradient_1).clone();
|
||||||
new_gradient.positions[0].1 = color;
|
new_gradient.positions[0].1 = None;
|
||||||
PropertiesPanelMessage::ModifyFill {
|
PropertiesPanelMessage::ModifyFill {
|
||||||
fill: Fill::LinearGradient(new_gradient),
|
fill: Fill::LinearGradient(new_gradient),
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
} else {
|
|
||||||
PropertiesPanelMessage::ResendActiveProperties.into()
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
})),
|
})),
|
||||||
@@ -534,17 +547,26 @@ fn node_section_fill(fill: &Fill) -> Option<LayoutRow> {
|
|||||||
direction: SeparatorDirection::Horizontal,
|
direction: SeparatorDirection::Horizontal,
|
||||||
})),
|
})),
|
||||||
WidgetHolder::new(Widget::ColorInput(ColorInput {
|
WidgetHolder::new(Widget::ColorInput(ColorInput {
|
||||||
value: gradient_2.positions[1].1.rgba_hex(),
|
value: gradient_2.positions[1].1.map(|color| color.rgba_hex()),
|
||||||
on_update: WidgetCallback::new(move |text_input: &ColorInput| {
|
on_update: WidgetCallback::new(move |text_input: &ColorInput| {
|
||||||
if let Some(color) = Color::from_rgba_str(&text_input.value).or_else(|| Color::from_rgb_str(&text_input.value)) {
|
if let Some(value) = &text_input.value {
|
||||||
|
if let Some(color) = Color::from_rgba_str(value).or_else(|| Color::from_rgb_str(value)) {
|
||||||
|
let mut new_gradient = (*gradient_2).clone();
|
||||||
|
new_gradient.positions[1].1 = Some(color);
|
||||||
|
PropertiesPanelMessage::ModifyFill {
|
||||||
|
fill: Fill::LinearGradient(new_gradient),
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
PropertiesPanelMessage::ResendActiveProperties.into()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
let mut new_gradient = (*gradient_2).clone();
|
let mut new_gradient = (*gradient_2).clone();
|
||||||
new_gradient.positions[1].1 = color;
|
new_gradient.positions[1].1 = None;
|
||||||
PropertiesPanelMessage::ModifyFill {
|
PropertiesPanelMessage::ModifyFill {
|
||||||
fill: Fill::LinearGradient(new_gradient),
|
fill: Fill::LinearGradient(new_gradient),
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
} else {
|
|
||||||
PropertiesPanelMessage::ResendActiveProperties.into()
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
})),
|
})),
|
||||||
@@ -553,7 +575,6 @@ fn node_section_fill(fill: &Fill) -> Option<LayoutRow> {
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Fill::None => None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -586,7 +607,7 @@ fn node_section_stroke(stroke: &Stroke) -> LayoutRow {
|
|||||||
direction: SeparatorDirection::Horizontal,
|
direction: SeparatorDirection::Horizontal,
|
||||||
})),
|
})),
|
||||||
WidgetHolder::new(Widget::ColorInput(ColorInput {
|
WidgetHolder::new(Widget::ColorInput(ColorInput {
|
||||||
value: stroke.color().rgba_hex(),
|
value: stroke.color().map(|color| color.rgba_hex()),
|
||||||
on_update: WidgetCallback::new(move |text_input: &ColorInput| {
|
on_update: WidgetCallback::new(move |text_input: &ColorInput| {
|
||||||
internal_stroke1
|
internal_stroke1
|
||||||
.clone()
|
.clone()
|
||||||
|
|||||||
@@ -92,8 +92,8 @@ impl MessageHandler<LayoutMessage, ()> for LayoutMessageHandler {
|
|||||||
responses.push_back(callback_message);
|
responses.push_back(callback_message);
|
||||||
}
|
}
|
||||||
Widget::ColorInput(color_input) => {
|
Widget::ColorInput(color_input) => {
|
||||||
let update_value = value.as_str().expect("ColorInput update was not of type: string");
|
let update_value = value.as_str().map(String::from);
|
||||||
color_input.value = update_value.into();
|
color_input.value = update_value;
|
||||||
let callback_message = (color_input.on_update.callback)(color_input);
|
let callback_message = (color_input.on_update.callback)(color_input);
|
||||||
responses.push_back(callback_message);
|
responses.push_back(callback_message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ pub struct TextInput {
|
|||||||
#[derive(Clone, Serialize, Deserialize, Derivative)]
|
#[derive(Clone, Serialize, Deserialize, Derivative)]
|
||||||
#[derivative(Debug, PartialEq, Default)]
|
#[derivative(Debug, PartialEq, Default)]
|
||||||
pub struct ColorInput {
|
pub struct ColorInput {
|
||||||
pub value: String,
|
pub value: Option<String>,
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||||
pub on_update: WidgetCallback<ColorInput>,
|
pub on_update: WidgetCallback<ColorInput>,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="floating-menu" :class="[direction.toLowerCase(), type.toLowerCase()]" v-if="open || type === 'Dialog'" ref="floatingMenu">
|
<div class="floating-menu" :class="[direction.toLowerCase(), type.toLowerCase()]" v-if="open || type === 'Dialog'" ref="floatingMenu">
|
||||||
<div class="tail" v-if="type === 'Popover'"></div>
|
<div class="tail" v-if="type === 'Popover'" ref="tail"></div>
|
||||||
<div class="floating-menu-container" ref="floatingMenuContainer">
|
<div class="floating-menu-container" ref="floatingMenuContainer">
|
||||||
<LayoutCol class="floating-menu-content" data-floating-menu-content :scrollableY="scrollableY" ref="floatingMenuContent" :style="floatingMenuContentStyle">
|
<LayoutCol class="floating-menu-content" data-floating-menu-content :scrollableY="scrollableY" ref="floatingMenuContent" :style="floatingMenuContentStyle">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
@@ -201,51 +201,74 @@ export default defineComponent({
|
|||||||
open: false,
|
open: false,
|
||||||
pointerStillDown: false,
|
pointerStillDown: false,
|
||||||
containerResizeObserver,
|
containerResizeObserver,
|
||||||
|
workspaceBounds: new DOMRect(),
|
||||||
|
floatingMenuBounds: new DOMRect(),
|
||||||
|
floatingMenuContentBounds: new DOMRect(),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
// Gets the client bounds of the elements and apply relevant styles to them
|
||||||
|
// TODO: Use the Vue :style attribute more whilst not causing recursive updates
|
||||||
updated() {
|
updated() {
|
||||||
|
const workspace = document.querySelector("[data-workspace]");
|
||||||
const floatingMenuContainer = this.$refs.floatingMenuContainer as HTMLElement;
|
const floatingMenuContainer = this.$refs.floatingMenuContainer as HTMLElement;
|
||||||
const floatingMenuContentComponent = this.$refs.floatingMenuContent as typeof LayoutCol;
|
const floatingMenuContentComponent = this.$refs.floatingMenuContent as typeof LayoutCol;
|
||||||
const floatingMenuContent = floatingMenuContentComponent && (floatingMenuContentComponent.$el as HTMLElement);
|
const floatingMenuContent = floatingMenuContentComponent && (floatingMenuContentComponent.$el as HTMLElement);
|
||||||
const workspace = document.querySelector("[data-workspace]");
|
const floatingMenu = this.$refs.floatingMenu as HTMLElement;
|
||||||
|
|
||||||
if (!floatingMenuContainer || !floatingMenuContentComponent || !floatingMenuContent || !workspace) return;
|
if (!workspace || !floatingMenuContainer || !floatingMenuContentComponent || !floatingMenuContent || !floatingMenu) return;
|
||||||
|
|
||||||
const workspaceBounds = workspace.getBoundingClientRect();
|
this.workspaceBounds = workspace.getBoundingClientRect();
|
||||||
const floatingMenuBounds = floatingMenuContent.getBoundingClientRect();
|
this.floatingMenuBounds = floatingMenu.getBoundingClientRect();
|
||||||
|
this.floatingMenuContentBounds = floatingMenuContent.getBoundingClientRect();
|
||||||
|
|
||||||
|
// Required to correctly position content when scrolled (it has a `position: fixed` to prevent clipping)
|
||||||
|
const tailOffset = this.type === "Popover" ? 10 : 0;
|
||||||
|
if (this.direction === "Bottom") floatingMenuContent.style.top = `${tailOffset + this.floatingMenuBounds.top}px`;
|
||||||
|
if (this.direction === "Top") floatingMenuContent.style.bottom = `${tailOffset + this.floatingMenuBounds.bottom}px`;
|
||||||
|
if (this.direction === "Right") floatingMenuContent.style.left = `${tailOffset + this.floatingMenuBounds.left}px`;
|
||||||
|
if (this.direction === "Left") floatingMenuContent.style.right = `${tailOffset + this.floatingMenuBounds.right}px`;
|
||||||
|
|
||||||
|
// Required to correctly position content when scrolled (it has a `position: fixed` to prevent clipping)
|
||||||
|
const tail = this.$refs.tail as HTMLElement;
|
||||||
|
if (tail) {
|
||||||
|
if (this.direction === "Bottom") tail.style.top = `${this.floatingMenuBounds.top}px`;
|
||||||
|
if (this.direction === "Top") tail.style.bottom = `${this.floatingMenuBounds.bottom}px`;
|
||||||
|
if (this.direction === "Right") tail.style.left = `${this.floatingMenuBounds.left}px`;
|
||||||
|
if (this.direction === "Left") tail.style.right = `${this.floatingMenuBounds.right}px`;
|
||||||
|
}
|
||||||
|
|
||||||
type Edge = "Top" | "Bottom" | "Left" | "Right";
|
type Edge = "Top" | "Bottom" | "Left" | "Right";
|
||||||
let zeroedBorderDirection1: Edge | undefined;
|
let zeroedBorderVertical: Edge | undefined;
|
||||||
let zeroedBorderDirection2: Edge | undefined;
|
let zeroedBorderHorizontal: Edge | undefined;
|
||||||
|
|
||||||
if (this.direction === "Top" || this.direction === "Bottom") {
|
if (this.direction === "Top" || this.direction === "Bottom") {
|
||||||
zeroedBorderDirection1 = this.direction === "Top" ? "Bottom" : "Top";
|
zeroedBorderVertical = this.direction === "Top" ? "Bottom" : "Top";
|
||||||
|
|
||||||
if (floatingMenuBounds.left - this.windowEdgeMargin <= workspaceBounds.left) {
|
if (this.floatingMenuContentBounds.left - this.windowEdgeMargin <= this.workspaceBounds.left) {
|
||||||
floatingMenuContent.style.left = `${this.windowEdgeMargin}px`;
|
floatingMenuContent.style.left = `${this.windowEdgeMargin}px`;
|
||||||
if (workspaceBounds.left + floatingMenuContainer.getBoundingClientRect().left === 12) zeroedBorderDirection2 = "Left";
|
if (this.workspaceBounds.left + floatingMenuContainer.getBoundingClientRect().left === 12) zeroedBorderHorizontal = "Left";
|
||||||
}
|
}
|
||||||
if (floatingMenuBounds.right + this.windowEdgeMargin >= workspaceBounds.right) {
|
if (this.floatingMenuContentBounds.right + this.windowEdgeMargin >= this.workspaceBounds.right) {
|
||||||
floatingMenuContent.style.right = `${this.windowEdgeMargin}px`;
|
floatingMenuContent.style.right = `${this.windowEdgeMargin}px`;
|
||||||
if (workspaceBounds.right - floatingMenuContainer.getBoundingClientRect().right === 12) zeroedBorderDirection2 = "Right";
|
if (this.workspaceBounds.right - floatingMenuContainer.getBoundingClientRect().right === 12) zeroedBorderHorizontal = "Right";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.direction === "Left" || this.direction === "Right") {
|
if (this.direction === "Left" || this.direction === "Right") {
|
||||||
zeroedBorderDirection2 = this.direction === "Left" ? "Right" : "Left";
|
zeroedBorderHorizontal = this.direction === "Left" ? "Right" : "Left";
|
||||||
|
|
||||||
if (floatingMenuBounds.top - this.windowEdgeMargin <= workspaceBounds.top) {
|
if (this.floatingMenuContentBounds.top - this.windowEdgeMargin <= this.workspaceBounds.top) {
|
||||||
floatingMenuContent.style.top = `${this.windowEdgeMargin}px`;
|
floatingMenuContent.style.top = `${this.windowEdgeMargin}px`;
|
||||||
if (workspaceBounds.top + floatingMenuContainer.getBoundingClientRect().top === 12) zeroedBorderDirection1 = "Top";
|
if (this.workspaceBounds.top + floatingMenuContainer.getBoundingClientRect().top === 12) zeroedBorderVertical = "Top";
|
||||||
}
|
}
|
||||||
if (floatingMenuBounds.bottom + this.windowEdgeMargin >= workspaceBounds.bottom) {
|
if (this.floatingMenuContentBounds.bottom + this.windowEdgeMargin >= this.workspaceBounds.bottom) {
|
||||||
floatingMenuContent.style.bottom = `${this.windowEdgeMargin}px`;
|
floatingMenuContent.style.bottom = `${this.windowEdgeMargin}px`;
|
||||||
if (workspaceBounds.bottom - floatingMenuContainer.getBoundingClientRect().bottom === 12) zeroedBorderDirection1 = "Bottom";
|
if (this.workspaceBounds.bottom - floatingMenuContainer.getBoundingClientRect().bottom === 12) zeroedBorderVertical = "Bottom";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the rounded corner from where the tail perfectly meets the corner
|
// Remove the rounded corner from the content where the tail perfectly meets the corner
|
||||||
if (this.type === "Popover" && this.windowEdgeMargin === 6 && zeroedBorderDirection1 && zeroedBorderDirection2) {
|
if (this.type === "Popover" && this.windowEdgeMargin === 6 && zeroedBorderVertical && zeroedBorderHorizontal) {
|
||||||
switch (`${zeroedBorderDirection1}${zeroedBorderDirection2}`) {
|
switch (`${zeroedBorderVertical}${zeroedBorderHorizontal}`) {
|
||||||
case "TopLeft":
|
case "TopLeft":
|
||||||
floatingMenuContent.style.borderTopLeftRadius = "0";
|
floatingMenuContent.style.borderTopLeftRadius = "0";
|
||||||
break;
|
break;
|
||||||
@@ -375,6 +398,7 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Switching from open to closed
|
// Switching from open to closed
|
||||||
if (!newState && oldState) {
|
if (!newState && oldState) {
|
||||||
window.removeEventListener("pointermove", this.pointerMoveHandler);
|
window.removeEventListener("pointermove", this.pointerMoveHandler);
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<LayoutRow class="color-input">
|
<LayoutRow class="color-input">
|
||||||
<TextInput :value="displayValue" :label="label" :disabled="disabled" @commitText="(value: string) => textInputUpdated(value)" :center="true" />
|
<OptionalInput :icon="'CloseX'" :checked="!!value" @update:checked="(val) => updateEnabled(val)"></OptionalInput>
|
||||||
|
<TextInput :value="displayValue" :label="label" :disabled="disabled || !value" @commitText="(value: string) => textInputUpdated(value)" :center="true" />
|
||||||
<Separator :type="'Related'" />
|
<Separator :type="'Related'" />
|
||||||
<LayoutRow class="swatch">
|
<LayoutRow class="swatch">
|
||||||
<button class="swatch-button" @click="() => menuOpen()" :style="`--swatch-color: #${value}`"></button>
|
<button class="swatch-button" :class="{ 'disabled-swatch': !value }" :style="`--swatch-color: #${value}`" @click="() => menuOpen()"></button>
|
||||||
<FloatingMenu :type="'Popover'" :direction="'Bottom'" horizontal ref="colorFloatingMenu">
|
<FloatingMenu :type="'Popover'" :direction="'Bottom'" horizontal ref="colorFloatingMenu">
|
||||||
<ColorPicker @update:color="(color) => colorPickerUpdated(color)" :color="color" />
|
<ColorPicker @update:color="(color) => colorPickerUpdated(color)" :color="color" />
|
||||||
</FloatingMenu>
|
</FloatingMenu>
|
||||||
@@ -44,6 +45,17 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
background: var(--swatch-color);
|
background: var(--swatch-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.disabled-swatch::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
border-top: 4px solid red;
|
||||||
|
width: 33px;
|
||||||
|
left: 22px;
|
||||||
|
top: -4px;
|
||||||
|
transform: rotate(135deg);
|
||||||
|
transform-origin: 0% 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.floating-menu {
|
.floating-menu {
|
||||||
@@ -63,18 +75,21 @@ import { RGBA } from "@/dispatcher/js-messages";
|
|||||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||||
import ColorPicker from "@/components/widgets/floating-menus/ColorPicker.vue";
|
import ColorPicker from "@/components/widgets/floating-menus/ColorPicker.vue";
|
||||||
import FloatingMenu from "@/components/widgets/floating-menus/FloatingMenu.vue";
|
import FloatingMenu from "@/components/widgets/floating-menus/FloatingMenu.vue";
|
||||||
|
import OptionalInput from "@/components/widgets/inputs/OptionalInput.vue";
|
||||||
import TextInput from "@/components/widgets/inputs/TextInput.vue";
|
import TextInput from "@/components/widgets/inputs/TextInput.vue";
|
||||||
import Separator from "@/components/widgets/separators/Separator.vue";
|
import Separator from "@/components/widgets/separators/Separator.vue";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
emits: ["update:value"],
|
emits: ["update:value"],
|
||||||
props: {
|
props: {
|
||||||
value: { type: String as PropType<string>, required: true },
|
value: { type: String as PropType<string | undefined>, required: true },
|
||||||
label: { type: String as PropType<string>, required: false },
|
label: { type: String as PropType<string>, required: false },
|
||||||
disabled: { type: Boolean as PropType<boolean>, default: false },
|
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
color() {
|
color() {
|
||||||
|
if (!this.value) return { r: 0, g: 0, b: 0, a: 1 };
|
||||||
|
|
||||||
const r = parseInt(this.value.slice(0, 2), 16);
|
const r = parseInt(this.value.slice(0, 2), 16);
|
||||||
const g = parseInt(this.value.slice(2, 4), 16);
|
const g = parseInt(this.value.slice(2, 4), 16);
|
||||||
const b = parseInt(this.value.slice(4, 6), 16);
|
const b = parseInt(this.value.slice(4, 6), 16);
|
||||||
@@ -82,6 +97,8 @@ export default defineComponent({
|
|||||||
return { r, g, b, a: a / 255 };
|
return { r, g, b, a: a / 255 };
|
||||||
},
|
},
|
||||||
displayValue() {
|
displayValue() {
|
||||||
|
if (!this.value) return "";
|
||||||
|
|
||||||
const value = this.value.toLowerCase();
|
const value = this.value.toLowerCase();
|
||||||
const shortenedIfOpaque = value.slice(-2) === "ff" ? value.slice(0, 6) : value;
|
const shortenedIfOpaque = value.slice(-2) === "ff" ? value.slice(0, 6) : value;
|
||||||
return `#${shortenedIfOpaque}`;
|
return `#${shortenedIfOpaque}`;
|
||||||
@@ -106,15 +123,23 @@ export default defineComponent({
|
|||||||
.map((byte) => `${byte}${byte}`)
|
.map((byte) => `${byte}${byte}`)
|
||||||
.concat("ff")
|
.concat("ff")
|
||||||
.join("");
|
.join("");
|
||||||
} else if (match.length === 6) sanitized = `${match}ff`;
|
} else if (match.length === 6) {
|
||||||
else if (match.length === 8) sanitized = match;
|
sanitized = `${match}ff`;
|
||||||
else return;
|
} else if (match.length === 8) {
|
||||||
|
sanitized = match;
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.$emit("update:value", sanitized);
|
this.$emit("update:value", sanitized);
|
||||||
},
|
},
|
||||||
menuOpen() {
|
menuOpen() {
|
||||||
(this.$refs.colorFloatingMenu as typeof FloatingMenu).setOpen();
|
(this.$refs.colorFloatingMenu as typeof FloatingMenu).setOpen();
|
||||||
},
|
},
|
||||||
|
updateEnabled(value: boolean) {
|
||||||
|
if (value) this.$emit("update:value", "000000");
|
||||||
|
else this.$emit("update:value", undefined);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
TextInput,
|
TextInput,
|
||||||
@@ -122,6 +147,7 @@ export default defineComponent({
|
|||||||
LayoutRow,
|
LayoutRow,
|
||||||
FloatingMenu,
|
FloatingMenu,
|
||||||
Separator,
|
Separator,
|
||||||
|
OptionalInput,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.optional-input {
|
.optional-input {
|
||||||
|
flex-grow: 0;
|
||||||
|
|
||||||
label {
|
label {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ pub struct Gradient {
|
|||||||
pub start: DVec2,
|
pub start: DVec2,
|
||||||
pub end: DVec2,
|
pub end: DVec2,
|
||||||
pub transform: DAffine2,
|
pub transform: DAffine2,
|
||||||
pub positions: Vec<(f64, Color)>,
|
pub positions: Vec<(f64, Option<Color>)>,
|
||||||
uuid: u64,
|
uuid: u64,
|
||||||
}
|
}
|
||||||
impl Gradient {
|
impl Gradient {
|
||||||
@@ -54,7 +54,7 @@ impl Gradient {
|
|||||||
Gradient {
|
Gradient {
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
positions: vec![(0., start_color), (1., end_color)],
|
positions: vec![(0., Some(start_color)), (1., Some(end_color))],
|
||||||
transform,
|
transform,
|
||||||
uuid,
|
uuid,
|
||||||
}
|
}
|
||||||
@@ -65,6 +65,7 @@ impl Gradient {
|
|||||||
let positions = self
|
let positions = self
|
||||||
.positions
|
.positions
|
||||||
.iter()
|
.iter()
|
||||||
|
.filter_map(|(pos, color)| color.map(|color| (pos, color)))
|
||||||
.map(|(position, color)| format!(r##"<stop offset="{}" stop-color="#{}" />"##, position, color.rgba_hex()))
|
.map(|(position, color)| format!(r##"<stop offset="{}" stop-color="#{}" />"##, position, color.rgba_hex()))
|
||||||
.collect::<String>();
|
.collect::<String>();
|
||||||
|
|
||||||
@@ -116,7 +117,7 @@ impl Fill {
|
|||||||
Self::None => Color::BLACK,
|
Self::None => Color::BLACK,
|
||||||
Self::Solid(color) => *color,
|
Self::Solid(color) => *color,
|
||||||
// TODO: Should correctly sample the gradient
|
// TODO: Should correctly sample the gradient
|
||||||
Self::LinearGradient(Gradient { positions, .. }) => positions[0].1,
|
Self::LinearGradient(Gradient { positions, .. }) => positions[0].1.unwrap_or(Color::BLACK),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,7 +180,7 @@ impl Display for LineJoin {
|
|||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct Stroke {
|
pub struct Stroke {
|
||||||
/// Stroke color
|
/// Stroke color
|
||||||
color: Color,
|
color: Option<Color>,
|
||||||
/// Line thickness
|
/// Line thickness
|
||||||
width: f32,
|
width: f32,
|
||||||
dash_lengths: Vec<f32>,
|
dash_lengths: Vec<f32>,
|
||||||
@@ -191,11 +192,15 @@ pub struct Stroke {
|
|||||||
|
|
||||||
impl Stroke {
|
impl Stroke {
|
||||||
pub fn new(color: Color, width: f32) -> Self {
|
pub fn new(color: Color, width: f32) -> Self {
|
||||||
Self { color, width, ..Default::default() }
|
Self {
|
||||||
|
color: Some(color),
|
||||||
|
width,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the current stroke color.
|
/// Get the current stroke color.
|
||||||
pub fn color(&self) -> Color {
|
pub fn color(&self) -> Option<Color> {
|
||||||
self.color
|
self.color
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,24 +231,33 @@ impl Stroke {
|
|||||||
|
|
||||||
/// Provide the SVG attributes for the stroke.
|
/// Provide the SVG attributes for the stroke.
|
||||||
pub fn render(&self) -> String {
|
pub fn render(&self) -> String {
|
||||||
format!(
|
if let Some(color) = self.color {
|
||||||
r##" stroke="#{}"{} stroke-width="{}" stroke-dasharray="{}" stroke-dashoffset="{}" stroke-linecap="{}" stroke-linejoin="{}" stroke-miterlimit="{}" "##,
|
format!(
|
||||||
self.color.rgb_hex(),
|
r##" stroke="#{}"{} stroke-width="{}" stroke-dasharray="{}" stroke-dashoffset="{}" stroke-linecap="{}" stroke-linejoin="{}" stroke-miterlimit="{}" "##,
|
||||||
format_opacity("stroke", self.color.a()),
|
color.rgb_hex(),
|
||||||
self.width,
|
format_opacity("stroke", color.a()),
|
||||||
self.dash_lengths(),
|
self.width,
|
||||||
self.dash_offset,
|
self.dash_lengths(),
|
||||||
self.line_cap,
|
self.dash_offset,
|
||||||
self.line_join,
|
self.line_cap,
|
||||||
self.miter_limit
|
self.line_join,
|
||||||
)
|
self.miter_limit
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_color(mut self, color: &str) -> Option<Self> {
|
pub fn with_color(mut self, color: &Option<String>) -> Option<Self> {
|
||||||
Color::from_rgba_str(color).or_else(|| Color::from_rgb_str(color)).map(|color| {
|
if let Some(color) = color {
|
||||||
self.color = color;
|
Color::from_rgba_str(color).or_else(|| Color::from_rgb_str(color)).map(|color| {
|
||||||
self
|
self.color = Some(color);
|
||||||
})
|
self
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
self.color = None;
|
||||||
|
Some(self)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_width(mut self, width: f32) -> Self {
|
pub fn with_width(mut self, width: f32) -> Self {
|
||||||
@@ -290,7 +304,7 @@ impl Default for Stroke {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
width: 0.,
|
width: 0.,
|
||||||
color: Color::from_rgba8(0, 0, 0, 255),
|
color: Some(Color::from_rgba8(0, 0, 0, 255)),
|
||||||
dash_lengths: vec![0.],
|
dash_lengths: vec![0.],
|
||||||
dash_offset: 0.,
|
dash_offset: 0.,
|
||||||
line_cap: LineCap::Butt,
|
line_cap: LineCap::Butt,
|
||||||
|
|||||||
Reference in New Issue
Block a user