Skip to content

Commit 8bc85bd

Browse files
0HyperCubeKeavon
andauthored
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>
1 parent 87e723b commit 8bc85bd

File tree

7 files changed

+161
-74
lines changed

7 files changed

+161
-74
lines changed

editor/src/document/properties_panel_message_handler.rs

Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -461,7 +461,7 @@ fn node_section_transform(layer: &Layer) -> LayoutRow {
461461

462462
fn node_section_fill(fill: &Fill) -> Option<LayoutRow> {
463463
match fill {
464-
Fill::Solid(color) => Some(LayoutRow::Section {
464+
Fill::Solid(_) | Fill::None => Some(LayoutRow::Section {
465465
name: "Fill".into(),
466466
layout: vec![LayoutRow::Row {
467467
name: "".into(),
@@ -475,13 +475,17 @@ fn node_section_fill(fill: &Fill) -> Option<LayoutRow> {
475475
direction: SeparatorDirection::Horizontal,
476476
})),
477477
WidgetHolder::new(Widget::ColorInput(ColorInput {
478-
value: color.rgba_hex(),
478+
value: if let Fill::Solid(color) = fill { Some(color.rgba_hex()) } else { None },
479479
on_update: WidgetCallback::new(|text_input: &ColorInput| {
480-
if let Some(color) = Color::from_rgba_str(&text_input.value).or_else(|| Color::from_rgb_str(&text_input.value)) {
481-
let new_fill = Fill::Solid(color);
482-
PropertiesPanelMessage::ModifyFill { fill: new_fill }.into()
480+
if let Some(value) = &text_input.value {
481+
if let Some(color) = Color::from_rgba_str(value).or_else(|| Color::from_rgb_str(value)) {
482+
let new_fill = Fill::Solid(color);
483+
PropertiesPanelMessage::ModifyFill { fill: new_fill }.into()
484+
} else {
485+
PropertiesPanelMessage::ResendActiveProperties.into()
486+
}
483487
} else {
484-
PropertiesPanelMessage::ResendActiveProperties.into()
488+
PropertiesPanelMessage::ModifyFill { fill: Fill::None }.into()
485489
}
486490
}),
487491
})),
@@ -506,17 +510,26 @@ fn node_section_fill(fill: &Fill) -> Option<LayoutRow> {
506510
direction: SeparatorDirection::Horizontal,
507511
})),
508512
WidgetHolder::new(Widget::ColorInput(ColorInput {
509-
value: gradient_1.positions[0].1.rgba_hex(),
513+
value: gradient_1.positions[0].1.map(|color| color.rgba_hex()),
510514
on_update: WidgetCallback::new(move |text_input: &ColorInput| {
511-
if let Some(color) = Color::from_rgba_str(&text_input.value).or_else(|| Color::from_rgb_str(&text_input.value)) {
515+
if let Some(value) = &text_input.value {
516+
if let Some(color) = Color::from_rgba_str(value).or_else(|| Color::from_rgb_str(value)) {
517+
let mut new_gradient = (*gradient_1).clone();
518+
new_gradient.positions[0].1 = Some(color);
519+
PropertiesPanelMessage::ModifyFill {
520+
fill: Fill::LinearGradient(new_gradient),
521+
}
522+
.into()
523+
} else {
524+
PropertiesPanelMessage::ResendActiveProperties.into()
525+
}
526+
} else {
512527
let mut new_gradient = (*gradient_1).clone();
513-
new_gradient.positions[0].1 = color;
528+
new_gradient.positions[0].1 = None;
514529
PropertiesPanelMessage::ModifyFill {
515530
fill: Fill::LinearGradient(new_gradient),
516531
}
517532
.into()
518-
} else {
519-
PropertiesPanelMessage::ResendActiveProperties.into()
520533
}
521534
}),
522535
})),
@@ -534,17 +547,26 @@ fn node_section_fill(fill: &Fill) -> Option<LayoutRow> {
534547
direction: SeparatorDirection::Horizontal,
535548
})),
536549
WidgetHolder::new(Widget::ColorInput(ColorInput {
537-
value: gradient_2.positions[1].1.rgba_hex(),
550+
value: gradient_2.positions[1].1.map(|color| color.rgba_hex()),
538551
on_update: WidgetCallback::new(move |text_input: &ColorInput| {
539-
if let Some(color) = Color::from_rgba_str(&text_input.value).or_else(|| Color::from_rgb_str(&text_input.value)) {
552+
if let Some(value) = &text_input.value {
553+
if let Some(color) = Color::from_rgba_str(value).or_else(|| Color::from_rgb_str(value)) {
554+
let mut new_gradient = (*gradient_2).clone();
555+
new_gradient.positions[1].1 = Some(color);
556+
PropertiesPanelMessage::ModifyFill {
557+
fill: Fill::LinearGradient(new_gradient),
558+
}
559+
.into()
560+
} else {
561+
PropertiesPanelMessage::ResendActiveProperties.into()
562+
}
563+
} else {
540564
let mut new_gradient = (*gradient_2).clone();
541-
new_gradient.positions[1].1 = color;
565+
new_gradient.positions[1].1 = None;
542566
PropertiesPanelMessage::ModifyFill {
543567
fill: Fill::LinearGradient(new_gradient),
544568
}
545569
.into()
546-
} else {
547-
PropertiesPanelMessage::ResendActiveProperties.into()
548570
}
549571
}),
550572
})),
@@ -553,7 +575,6 @@ fn node_section_fill(fill: &Fill) -> Option<LayoutRow> {
553575
],
554576
})
555577
}
556-
Fill::None => None,
557578
}
558579
}
559580

@@ -586,7 +607,7 @@ fn node_section_stroke(stroke: &Stroke) -> LayoutRow {
586607
direction: SeparatorDirection::Horizontal,
587608
})),
588609
WidgetHolder::new(Widget::ColorInput(ColorInput {
589-
value: stroke.color().rgba_hex(),
610+
value: stroke.color().map(|color| color.rgba_hex()),
590611
on_update: WidgetCallback::new(move |text_input: &ColorInput| {
591612
internal_stroke1
592613
.clone()

editor/src/layout/layout_message_handler.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,8 @@ impl MessageHandler<LayoutMessage, ()> for LayoutMessageHandler {
9292
responses.push_back(callback_message);
9393
}
9494
Widget::ColorInput(color_input) => {
95-
let update_value = value.as_str().expect("ColorInput update was not of type: string");
96-
color_input.value = update_value.into();
95+
let update_value = value.as_str().map(String::from);
96+
color_input.value = update_value;
9797
let callback_message = (color_input.on_update.callback)(color_input);
9898
responses.push_back(callback_message);
9999
}

editor/src/layout/widgets.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ pub struct TextInput {
203203
#[derive(Clone, Serialize, Deserialize, Derivative)]
204204
#[derivative(Debug, PartialEq, Default)]
205205
pub struct ColorInput {
206-
pub value: String,
206+
pub value: Option<String>,
207207
#[serde(skip)]
208208
#[derivative(Debug = "ignore", PartialEq = "ignore")]
209209
pub on_update: WidgetCallback<ColorInput>,

frontend/src/components/widgets/floating-menus/FloatingMenu.vue

Lines changed: 46 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<template>
22
<div class="floating-menu" :class="[direction.toLowerCase(), type.toLowerCase()]" v-if="open || type === 'Dialog'" ref="floatingMenu">
3-
<div class="tail" v-if="type === 'Popover'"></div>
3+
<div class="tail" v-if="type === 'Popover'" ref="tail"></div>
44
<div class="floating-menu-container" ref="floatingMenuContainer">
55
<LayoutCol class="floating-menu-content" data-floating-menu-content :scrollableY="scrollableY" ref="floatingMenuContent" :style="floatingMenuContentStyle">
66
<slot></slot>
@@ -201,51 +201,74 @@ export default defineComponent({
201201
open: false,
202202
pointerStillDown: false,
203203
containerResizeObserver,
204+
workspaceBounds: new DOMRect(),
205+
floatingMenuBounds: new DOMRect(),
206+
floatingMenuContentBounds: new DOMRect(),
204207
};
205208
},
209+
// Gets the client bounds of the elements and apply relevant styles to them
210+
// TODO: Use the Vue :style attribute more whilst not causing recursive updates
206211
updated() {
212+
const workspace = document.querySelector("[data-workspace]");
207213
const floatingMenuContainer = this.$refs.floatingMenuContainer as HTMLElement;
208214
const floatingMenuContentComponent = this.$refs.floatingMenuContent as typeof LayoutCol;
209215
const floatingMenuContent = floatingMenuContentComponent && (floatingMenuContentComponent.$el as HTMLElement);
210-
const workspace = document.querySelector("[data-workspace]");
211-
212-
if (!floatingMenuContainer || !floatingMenuContentComponent || !floatingMenuContent || !workspace) return;
213-
214-
const workspaceBounds = workspace.getBoundingClientRect();
215-
const floatingMenuBounds = floatingMenuContent.getBoundingClientRect();
216+
const floatingMenu = this.$refs.floatingMenu as HTMLElement;
217+
218+
if (!workspace || !floatingMenuContainer || !floatingMenuContentComponent || !floatingMenuContent || !floatingMenu) return;
219+
220+
this.workspaceBounds = workspace.getBoundingClientRect();
221+
this.floatingMenuBounds = floatingMenu.getBoundingClientRect();
222+
this.floatingMenuContentBounds = floatingMenuContent.getBoundingClientRect();
223+
224+
// Required to correctly position content when scrolled (it has a `position: fixed` to prevent clipping)
225+
const tailOffset = this.type === "Popover" ? 10 : 0;
226+
if (this.direction === "Bottom") floatingMenuContent.style.top = `${tailOffset + this.floatingMenuBounds.top}px`;
227+
if (this.direction === "Top") floatingMenuContent.style.bottom = `${tailOffset + this.floatingMenuBounds.bottom}px`;
228+
if (this.direction === "Right") floatingMenuContent.style.left = `${tailOffset + this.floatingMenuBounds.left}px`;
229+
if (this.direction === "Left") floatingMenuContent.style.right = `${tailOffset + this.floatingMenuBounds.right}px`;
230+
231+
// Required to correctly position content when scrolled (it has a `position: fixed` to prevent clipping)
232+
const tail = this.$refs.tail as HTMLElement;
233+
if (tail) {
234+
if (this.direction === "Bottom") tail.style.top = `${this.floatingMenuBounds.top}px`;
235+
if (this.direction === "Top") tail.style.bottom = `${this.floatingMenuBounds.bottom}px`;
236+
if (this.direction === "Right") tail.style.left = `${this.floatingMenuBounds.left}px`;
237+
if (this.direction === "Left") tail.style.right = `${this.floatingMenuBounds.right}px`;
238+
}
216239
217240
type Edge = "Top" | "Bottom" | "Left" | "Right";
218-
let zeroedBorderDirection1: Edge | undefined;
219-
let zeroedBorderDirection2: Edge | undefined;
241+
let zeroedBorderVertical: Edge | undefined;
242+
let zeroedBorderHorizontal: Edge | undefined;
220243
221244
if (this.direction === "Top" || this.direction === "Bottom") {
222-
zeroedBorderDirection1 = this.direction === "Top" ? "Bottom" : "Top";
245+
zeroedBorderVertical = this.direction === "Top" ? "Bottom" : "Top";
223246
224-
if (floatingMenuBounds.left - this.windowEdgeMargin <= workspaceBounds.left) {
247+
if (this.floatingMenuContentBounds.left - this.windowEdgeMargin <= this.workspaceBounds.left) {
225248
floatingMenuContent.style.left = `${this.windowEdgeMargin}px`;
226-
if (workspaceBounds.left + floatingMenuContainer.getBoundingClientRect().left === 12) zeroedBorderDirection2 = "Left";
249+
if (this.workspaceBounds.left + floatingMenuContainer.getBoundingClientRect().left === 12) zeroedBorderHorizontal = "Left";
227250
}
228-
if (floatingMenuBounds.right + this.windowEdgeMargin >= workspaceBounds.right) {
251+
if (this.floatingMenuContentBounds.right + this.windowEdgeMargin >= this.workspaceBounds.right) {
229252
floatingMenuContent.style.right = `${this.windowEdgeMargin}px`;
230-
if (workspaceBounds.right - floatingMenuContainer.getBoundingClientRect().right === 12) zeroedBorderDirection2 = "Right";
253+
if (this.workspaceBounds.right - floatingMenuContainer.getBoundingClientRect().right === 12) zeroedBorderHorizontal = "Right";
231254
}
232255
}
233256
if (this.direction === "Left" || this.direction === "Right") {
234-
zeroedBorderDirection2 = this.direction === "Left" ? "Right" : "Left";
257+
zeroedBorderHorizontal = this.direction === "Left" ? "Right" : "Left";
235258
236-
if (floatingMenuBounds.top - this.windowEdgeMargin <= workspaceBounds.top) {
259+
if (this.floatingMenuContentBounds.top - this.windowEdgeMargin <= this.workspaceBounds.top) {
237260
floatingMenuContent.style.top = `${this.windowEdgeMargin}px`;
238-
if (workspaceBounds.top + floatingMenuContainer.getBoundingClientRect().top === 12) zeroedBorderDirection1 = "Top";
261+
if (this.workspaceBounds.top + floatingMenuContainer.getBoundingClientRect().top === 12) zeroedBorderVertical = "Top";
239262
}
240-
if (floatingMenuBounds.bottom + this.windowEdgeMargin >= workspaceBounds.bottom) {
263+
if (this.floatingMenuContentBounds.bottom + this.windowEdgeMargin >= this.workspaceBounds.bottom) {
241264
floatingMenuContent.style.bottom = `${this.windowEdgeMargin}px`;
242-
if (workspaceBounds.bottom - floatingMenuContainer.getBoundingClientRect().bottom === 12) zeroedBorderDirection1 = "Bottom";
265+
if (this.workspaceBounds.bottom - floatingMenuContainer.getBoundingClientRect().bottom === 12) zeroedBorderVertical = "Bottom";
243266
}
244267
}
245268
246-
// Remove the rounded corner from where the tail perfectly meets the corner
247-
if (this.type === "Popover" && this.windowEdgeMargin === 6 && zeroedBorderDirection1 && zeroedBorderDirection2) {
248-
switch (`${zeroedBorderDirection1}${zeroedBorderDirection2}`) {
269+
// Remove the rounded corner from the content where the tail perfectly meets the corner
270+
if (this.type === "Popover" && this.windowEdgeMargin === 6 && zeroedBorderVertical && zeroedBorderHorizontal) {
271+
switch (`${zeroedBorderVertical}${zeroedBorderHorizontal}`) {
249272
case "TopLeft":
250273
floatingMenuContent.style.borderTopLeftRadius = "0";
251274
break;
@@ -375,6 +398,7 @@ export default defineComponent({
375398
}
376399
});
377400
}
401+
378402
// Switching from open to closed
379403
if (!newState && oldState) {
380404
window.removeEventListener("pointermove", this.pointerMoveHandler);

frontend/src/components/widgets/inputs/ColorInput.vue

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
<template>
22
<LayoutRow class="color-input">
3-
<TextInput :value="displayValue" :label="label" :disabled="disabled" @commitText="(value: string) => textInputUpdated(value)" :center="true" />
3+
<OptionalInput :icon="'CloseX'" :checked="!!value" @update:checked="(val) => updateEnabled(val)"></OptionalInput>
4+
<TextInput :value="displayValue" :label="label" :disabled="disabled || !value" @commitText="(value: string) => textInputUpdated(value)" :center="true" />
45
<Separator :type="'Related'" />
56
<LayoutRow class="swatch">
6-
<button class="swatch-button" @click="() => menuOpen()" :style="`--swatch-color: #${value}`"></button>
7+
<button class="swatch-button" :class="{ 'disabled-swatch': !value }" :style="`--swatch-color: #${value}`" @click="() => menuOpen()"></button>
78
<FloatingMenu :type="'Popover'" :direction="'Bottom'" horizontal ref="colorFloatingMenu">
89
<ColorPicker @update:color="(color) => colorPickerUpdated(color)" :color="color" />
910
</FloatingMenu>
@@ -44,6 +45,17 @@
4445
height: 100%;
4546
background: var(--swatch-color);
4647
}
48+
49+
&.disabled-swatch::after {
50+
content: "";
51+
position: absolute;
52+
border-top: 4px solid red;
53+
width: 33px;
54+
left: 22px;
55+
top: -4px;
56+
transform: rotate(135deg);
57+
transform-origin: 0% 100%;
58+
}
4759
}
4860
4961
.floating-menu {
@@ -63,25 +75,30 @@ import { RGBA } from "@/dispatcher/js-messages";
6375
import LayoutRow from "@/components/layout/LayoutRow.vue";
6476
import ColorPicker from "@/components/widgets/floating-menus/ColorPicker.vue";
6577
import FloatingMenu from "@/components/widgets/floating-menus/FloatingMenu.vue";
78+
import OptionalInput from "@/components/widgets/inputs/OptionalInput.vue";
6679
import TextInput from "@/components/widgets/inputs/TextInput.vue";
6780
import Separator from "@/components/widgets/separators/Separator.vue";
6881
6982
export default defineComponent({
7083
emits: ["update:value"],
7184
props: {
72-
value: { type: String as PropType<string>, required: true },
85+
value: { type: String as PropType<string | undefined>, required: true },
7386
label: { type: String as PropType<string>, required: false },
7487
disabled: { type: Boolean as PropType<boolean>, default: false },
7588
},
7689
computed: {
7790
color() {
91+
if (!this.value) return { r: 0, g: 0, b: 0, a: 1 };
92+
7893
const r = parseInt(this.value.slice(0, 2), 16);
7994
const g = parseInt(this.value.slice(2, 4), 16);
8095
const b = parseInt(this.value.slice(4, 6), 16);
8196
const a = parseInt(this.value.slice(6, 8), 16);
8297
return { r, g, b, a: a / 255 };
8398
},
8499
displayValue() {
100+
if (!this.value) return "";
101+
85102
const value = this.value.toLowerCase();
86103
const shortenedIfOpaque = value.slice(-2) === "ff" ? value.slice(0, 6) : value;
87104
return `#${shortenedIfOpaque}`;
@@ -106,22 +123,31 @@ export default defineComponent({
106123
.map((byte) => `${byte}${byte}`)
107124
.concat("ff")
108125
.join("");
109-
} else if (match.length === 6) sanitized = `${match}ff`;
110-
else if (match.length === 8) sanitized = match;
111-
else return;
126+
} else if (match.length === 6) {
127+
sanitized = `${match}ff`;
128+
} else if (match.length === 8) {
129+
sanitized = match;
130+
} else {
131+
return;
132+
}
112133
113134
this.$emit("update:value", sanitized);
114135
},
115136
menuOpen() {
116137
(this.$refs.colorFloatingMenu as typeof FloatingMenu).setOpen();
117138
},
139+
updateEnabled(value: boolean) {
140+
if (value) this.$emit("update:value", "000000");
141+
else this.$emit("update:value", undefined);
142+
},
118143
},
119144
components: {
120145
TextInput,
121146
ColorPicker,
122147
LayoutRow,
123148
FloatingMenu,
124149
Separator,
150+
OptionalInput,
125151
},
126152
});
127153
</script>

frontend/src/components/widgets/inputs/OptionalInput.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
<style lang="scss">
88
.optional-input {
9+
flex-grow: 0;
10+
911
label {
1012
align-items: center;
1113
justify-content: center;

0 commit comments

Comments
 (0)