Skip to content

All shapes now have a Fill in the properties panel; color inputs are now optional #583

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Apr 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 39 additions & 18 deletions editor/src/document/properties_panel_message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,7 @@ fn node_section_transform(layer: &Layer) -> LayoutRow {

fn node_section_fill(fill: &Fill) -> Option<LayoutRow> {
match fill {
Fill::Solid(color) => Some(LayoutRow::Section {
Fill::Solid(_) | Fill::None => Some(LayoutRow::Section {
name: "Fill".into(),
layout: vec![LayoutRow::Row {
name: "".into(),
Expand All @@ -475,13 +475,17 @@ fn node_section_fill(fill: &Fill) -> Option<LayoutRow> {
direction: SeparatorDirection::Horizontal,
})),
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| {
if let Some(color) = Color::from_rgba_str(&text_input.value).or_else(|| Color::from_rgb_str(&text_input.value)) {
let new_fill = Fill::Solid(color);
PropertiesPanelMessage::ModifyFill { fill: new_fill }.into()
if let Some(value) = &text_input.value {
if let Some(color) = Color::from_rgba_str(value).or_else(|| Color::from_rgb_str(value)) {
let new_fill = Fill::Solid(color);
PropertiesPanelMessage::ModifyFill { fill: new_fill }.into()
} else {
PropertiesPanelMessage::ResendActiveProperties.into()
}
} else {
PropertiesPanelMessage::ResendActiveProperties.into()
PropertiesPanelMessage::ModifyFill { fill: Fill::None }.into()
}
}),
})),
Expand All @@ -506,17 +510,26 @@ fn node_section_fill(fill: &Fill) -> Option<LayoutRow> {
direction: SeparatorDirection::Horizontal,
})),
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| {
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();
new_gradient.positions[0].1 = color;
new_gradient.positions[0].1 = None;
PropertiesPanelMessage::ModifyFill {
fill: Fill::LinearGradient(new_gradient),
}
.into()
} else {
PropertiesPanelMessage::ResendActiveProperties.into()
}
}),
})),
Expand All @@ -534,17 +547,26 @@ fn node_section_fill(fill: &Fill) -> Option<LayoutRow> {
direction: SeparatorDirection::Horizontal,
})),
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| {
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();
new_gradient.positions[1].1 = color;
new_gradient.positions[1].1 = None;
PropertiesPanelMessage::ModifyFill {
fill: Fill::LinearGradient(new_gradient),
}
.into()
} else {
PropertiesPanelMessage::ResendActiveProperties.into()
}
}),
})),
Expand All @@ -553,7 +575,6 @@ fn node_section_fill(fill: &Fill) -> Option<LayoutRow> {
],
})
}
Fill::None => None,
}
}

Expand Down Expand Up @@ -586,7 +607,7 @@ fn node_section_stroke(stroke: &Stroke) -> LayoutRow {
direction: SeparatorDirection::Horizontal,
})),
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| {
internal_stroke1
.clone()
Expand Down
4 changes: 2 additions & 2 deletions editor/src/layout/layout_message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ impl MessageHandler<LayoutMessage, ()> for LayoutMessageHandler {
responses.push_back(callback_message);
}
Widget::ColorInput(color_input) => {
let update_value = value.as_str().expect("ColorInput update was not of type: string");
color_input.value = update_value.into();
let update_value = value.as_str().map(String::from);
color_input.value = update_value;
let callback_message = (color_input.on_update.callback)(color_input);
responses.push_back(callback_message);
}
Expand Down
2 changes: 1 addition & 1 deletion editor/src/layout/widgets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ pub struct TextInput {
#[derive(Clone, Serialize, Deserialize, Derivative)]
#[derivative(Debug, PartialEq, Default)]
pub struct ColorInput {
pub value: String,
pub value: Option<String>,
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_update: WidgetCallback<ColorInput>,
Expand Down
68 changes: 46 additions & 22 deletions frontend/src/components/widgets/floating-menus/FloatingMenu.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<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">
<LayoutCol class="floating-menu-content" data-floating-menu-content :scrollableY="scrollableY" ref="floatingMenuContent" :style="floatingMenuContentStyle">
<slot></slot>
Expand Down Expand Up @@ -201,51 +201,74 @@ export default defineComponent({
open: false,
pointerStillDown: false,
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() {
const workspace = document.querySelector("[data-workspace]");
const floatingMenuContainer = this.$refs.floatingMenuContainer as HTMLElement;
const floatingMenuContentComponent = this.$refs.floatingMenuContent as typeof LayoutCol;
const floatingMenuContent = floatingMenuContentComponent && (floatingMenuContentComponent.$el as HTMLElement);
const workspace = document.querySelector("[data-workspace]");

if (!floatingMenuContainer || !floatingMenuContentComponent || !floatingMenuContent || !workspace) return;

const workspaceBounds = workspace.getBoundingClientRect();
const floatingMenuBounds = floatingMenuContent.getBoundingClientRect();
const floatingMenu = this.$refs.floatingMenu as HTMLElement;

if (!workspace || !floatingMenuContainer || !floatingMenuContentComponent || !floatingMenuContent || !floatingMenu) return;

this.workspaceBounds = workspace.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";
let zeroedBorderDirection1: Edge | undefined;
let zeroedBorderDirection2: Edge | undefined;
let zeroedBorderVertical: Edge | undefined;
let zeroedBorderHorizontal: Edge | undefined;

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`;
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`;
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") {
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`;
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`;
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
if (this.type === "Popover" && this.windowEdgeMargin === 6 && zeroedBorderDirection1 && zeroedBorderDirection2) {
switch (`${zeroedBorderDirection1}${zeroedBorderDirection2}`) {
// Remove the rounded corner from the content where the tail perfectly meets the corner
if (this.type === "Popover" && this.windowEdgeMargin === 6 && zeroedBorderVertical && zeroedBorderHorizontal) {
switch (`${zeroedBorderVertical}${zeroedBorderHorizontal}`) {
case "TopLeft":
floatingMenuContent.style.borderTopLeftRadius = "0";
break;
Expand Down Expand Up @@ -375,6 +398,7 @@ export default defineComponent({
}
});
}

// Switching from open to closed
if (!newState && oldState) {
window.removeEventListener("pointermove", this.pointerMoveHandler);
Expand Down
38 changes: 32 additions & 6 deletions frontend/src/components/widgets/inputs/ColorInput.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
<template>
<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'" />
<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">
<ColorPicker @update:color="(color) => colorPickerUpdated(color)" :color="color" />
</FloatingMenu>
Expand Down Expand Up @@ -44,6 +45,17 @@
height: 100%;
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 {
Expand All @@ -63,25 +75,30 @@ import { RGBA } from "@/dispatcher/js-messages";
import LayoutRow from "@/components/layout/LayoutRow.vue";
import ColorPicker from "@/components/widgets/floating-menus/ColorPicker.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 Separator from "@/components/widgets/separators/Separator.vue";

export default defineComponent({
emits: ["update:value"],
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 },
disabled: { type: Boolean as PropType<boolean>, default: false },
},
computed: {
color() {
if (!this.value) return { r: 0, g: 0, b: 0, a: 1 };

const r = parseInt(this.value.slice(0, 2), 16);
const g = parseInt(this.value.slice(2, 4), 16);
const b = parseInt(this.value.slice(4, 6), 16);
const a = parseInt(this.value.slice(6, 8), 16);
return { r, g, b, a: a / 255 };
},
displayValue() {
if (!this.value) return "";

const value = this.value.toLowerCase();
const shortenedIfOpaque = value.slice(-2) === "ff" ? value.slice(0, 6) : value;
return `#${shortenedIfOpaque}`;
Expand All @@ -106,22 +123,31 @@ export default defineComponent({
.map((byte) => `${byte}${byte}`)
.concat("ff")
.join("");
} else if (match.length === 6) sanitized = `${match}ff`;
else if (match.length === 8) sanitized = match;
else return;
} else if (match.length === 6) {
sanitized = `${match}ff`;
} else if (match.length === 8) {
sanitized = match;
} else {
return;
}

this.$emit("update:value", sanitized);
},
menuOpen() {
(this.$refs.colorFloatingMenu as typeof FloatingMenu).setOpen();
},
updateEnabled(value: boolean) {
if (value) this.$emit("update:value", "000000");
else this.$emit("update:value", undefined);
},
},
components: {
TextInput,
ColorPicker,
LayoutRow,
FloatingMenu,
Separator,
OptionalInput,
},
});
</script>
2 changes: 2 additions & 0 deletions frontend/src/components/widgets/inputs/OptionalInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

<style lang="scss">
.optional-input {
flex-grow: 0;

label {
align-items: center;
justify-content: center;
Expand Down
Loading