Skip to content

Commit c1137ee

Browse files
mfish33Keavon
andcommitted
Color Input (#565)
* initial working prototype * clean up component * Fix alignment * Code review tweaks Co-authored-by: Keavon Chambers <keavon@keavon.com>
1 parent 6cb1220 commit c1137ee

File tree

6 files changed

+126
-12
lines changed

6 files changed

+126
-12
lines changed

editor/src/document/properties_panel_message_handler.rs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use super::layer_panel::LayerDataTypeDiscriminant;
22
use crate::document::properties_panel_message::TransformOp;
33
use crate::layout::layout_message::LayoutTarget;
44
use crate::layout::widgets::{
5-
IconLabel, LayoutRow, NumberInput, PopoverButton, Separator, SeparatorDirection, SeparatorType, TextInput, TextLabel, Widget, WidgetCallback, WidgetHolder, WidgetLayout,
5+
ColorInput, IconLabel, LayoutRow, NumberInput, PopoverButton, Separator, SeparatorDirection, SeparatorType, TextInput, TextLabel, Widget, WidgetCallback, WidgetHolder, WidgetLayout,
66
};
77
use crate::message_prelude::*;
88

@@ -424,9 +424,9 @@ fn node_section_fill(fill: &Fill) -> Option<LayoutRow> {
424424
separator_type: SeparatorType::Related,
425425
direction: SeparatorDirection::Horizontal,
426426
})),
427-
WidgetHolder::new(Widget::TextInput(TextInput {
427+
WidgetHolder::new(Widget::ColorInput(ColorInput {
428428
value: color.rgba_hex(),
429-
on_update: WidgetCallback::new(|text_input: &TextInput| {
429+
on_update: WidgetCallback::new(|text_input: &ColorInput| {
430430
if let Some(color) = Color::from_rgba_str(&text_input.value).or(Color::from_rgb_str(&text_input.value)) {
431431
let new_fill = Fill::Solid(color);
432432
PropertiesPanelMessage::ModifyFill { fill: new_fill }.into()
@@ -455,9 +455,9 @@ fn node_section_fill(fill: &Fill) -> Option<LayoutRow> {
455455
separator_type: SeparatorType::Related,
456456
direction: SeparatorDirection::Horizontal,
457457
})),
458-
WidgetHolder::new(Widget::TextInput(TextInput {
458+
WidgetHolder::new(Widget::ColorInput(ColorInput {
459459
value: gradient_1.positions[0].1.rgba_hex(),
460-
on_update: WidgetCallback::new(move |text_input: &TextInput| {
460+
on_update: WidgetCallback::new(move |text_input: &ColorInput| {
461461
if let Some(color) = Color::from_rgba_str(&text_input.value).or(Color::from_rgb_str(&text_input.value)) {
462462
let mut new_gradient = (*gradient_1).clone();
463463
new_gradient.positions[0].1 = color;
@@ -483,9 +483,9 @@ fn node_section_fill(fill: &Fill) -> Option<LayoutRow> {
483483
separator_type: SeparatorType::Related,
484484
direction: SeparatorDirection::Horizontal,
485485
})),
486-
WidgetHolder::new(Widget::TextInput(TextInput {
486+
WidgetHolder::new(Widget::ColorInput(ColorInput {
487487
value: gradient_2.positions[1].1.rgba_hex(),
488-
on_update: WidgetCallback::new(move |text_input: &TextInput| {
488+
on_update: WidgetCallback::new(move |text_input: &ColorInput| {
489489
if let Some(color) = Color::from_rgba_str(&text_input.value).or(Color::from_rgb_str(&text_input.value)) {
490490
let mut new_gradient = (*gradient_2).clone();
491491
new_gradient.positions[1].1 = color;
@@ -524,9 +524,9 @@ fn node_section_stroke(stroke: &Stroke) -> LayoutRow {
524524
separator_type: SeparatorType::Related,
525525
direction: SeparatorDirection::Horizontal,
526526
})),
527-
WidgetHolder::new(Widget::TextInput(TextInput {
527+
WidgetHolder::new(Widget::ColorInput(ColorInput {
528528
value: stroke.color().rgba_hex(),
529-
on_update: WidgetCallback::new(move |text_input: &TextInput| {
529+
on_update: WidgetCallback::new(move |text_input: &ColorInput| {
530530
PropertiesPanelMessage::ModifyStroke {
531531
color: text_input.value.clone(),
532532
weight: weight as f64,

editor/src/layout/layout_message_handler.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,17 +80,23 @@ impl MessageHandler<LayoutMessage, ()> for LayoutMessageHandler {
8080
responses.push_back(callback_message);
8181
}
8282
Widget::RadioInput(radio_input) => {
83-
let update_value = value.as_u64().expect("OptionalInput update was not of type: u64");
83+
let update_value = value.as_u64().expect("RadioInput update was not of type: u64");
8484
radio_input.selected_index = update_value as u32;
8585
let callback_message = (radio_input.entries[update_value as usize].on_update.callback)(&());
8686
responses.push_back(callback_message);
8787
}
8888
Widget::TextInput(text_input) => {
89-
let update_value = value.as_str().expect("OptionalInput update was not of type: string");
89+
let update_value = value.as_str().expect("TextInput update was not of type: string");
9090
text_input.value = update_value.into();
9191
let callback_message = (text_input.on_update.callback)(text_input);
9292
responses.push_back(callback_message);
9393
}
94+
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();
97+
let callback_message = (color_input.on_update.callback)(color_input);
98+
responses.push_back(callback_message);
99+
}
94100
Widget::TextLabel(_) => {}
95101
};
96102
self.send_layout(layout_target, responses);

editor/src/layout/widgets.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ impl<T> Default for WidgetCallback<T> {
150150
#[remain::sorted]
151151
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
152152
pub enum Widget {
153+
ColorInput(ColorInput),
153154
IconButton(IconButton),
154155
IconLabel(IconLabel),
155156
NumberInput(NumberInput),
@@ -199,6 +200,15 @@ pub struct TextInput {
199200
pub on_update: WidgetCallback<TextInput>,
200201
}
201202

203+
#[derive(Clone, Serialize, Deserialize, Derivative)]
204+
#[derivative(Debug, PartialEq, Default)]
205+
pub struct ColorInput {
206+
pub value: String,
207+
#[serde(skip)]
208+
#[derivative(Debug = "ignore", PartialEq = "ignore")]
209+
pub on_update: WidgetCallback<ColorInput>,
210+
}
211+
202212
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
203213
pub enum NumberInputIncrementBehavior {
204214
Add,

frontend/src/components/widgets/WidgetRow.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
:incrementCallbackDecrease="() => updateLayout(component.widget_id, 'Decrement')"
1616
/>
1717
<TextInput v-if="component.kind === 'TextInput'" v-bind="component.props" @commitText="(value: string) => updateLayout(component.widget_id, value)" />
18+
<ColorInput v-if="component.kind === 'ColorInput'" v-bind="component.props" @update:value="(value: string) => updateLayout(component.widget_id, value)" />
1819
<IconButton v-if="component.kind === 'IconButton'" v-bind="component.props" :action="() => updateLayout(component.widget_id, null)" />
1920
<OptionalInput v-if="component.kind === 'OptionalInput'" v-bind="component.props" @update:checked="(value: boolean) => updateLayout(component.widget_id, value)" />
2021
<RadioInput v-if="component.kind === 'RadioInput'" v-bind="component.props" @update:selectedIndex="(value: number) => updateLayout(component.widget_id, value)" />
@@ -41,6 +42,7 @@ import { WidgetRow } from "@/dispatcher/js-messages";
4142
4243
import IconButton from "@/components/widgets/buttons/IconButton.vue";
4344
import PopoverButton from "@/components/widgets/buttons/PopoverButton.vue";
45+
import ColorInput from "@/components/widgets/inputs/ColorInput.vue";
4446
import NumberInput from "@/components/widgets/inputs/NumberInput.vue";
4547
import OptionalInput from "@/components/widgets/inputs/OptionalInput.vue";
4648
import RadioInput from "@/components/widgets/inputs/RadioInput.vue";
@@ -70,6 +72,7 @@ export default defineComponent({
7072
RadioInput,
7173
TextLabel,
7274
IconLabel,
75+
ColorInput,
7376
},
7477
});
7578
</script>
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<template>
2+
<LayoutRow class="color-input">
3+
<TextInput :value="value" :label="label" :disabled="disabled" @commitText="(value: string) => textInputUpdated(value)" :center="true" />
4+
<Separator :type="'Related'" />
5+
<LayoutRow class="swatch">
6+
<button class="swatch-button" @click="() => menuOpen()" :style="{ background: `#${value}` }"></button>
7+
<FloatingMenu :type="'Popover'" :direction="'Bottom'" horizontal ref="colorFloatingMenu">
8+
<ColorPicker @update:color="(color) => colorPickerUpdated(color)" :color="color" />
9+
</FloatingMenu>
10+
</LayoutRow>
11+
</LayoutRow>
12+
</template>
13+
14+
<style lang="scss">
15+
.color-input {
16+
.text-input input {
17+
text-align: center;
18+
}
19+
20+
.swatch {
21+
flex: 0 0 auto;
22+
position: relative;
23+
24+
.swatch-button {
25+
height: 24px;
26+
width: 24px;
27+
bottom: 0;
28+
left: 50%;
29+
padding: 0;
30+
outline: none;
31+
border: none;
32+
border-radius: 2px;
33+
}
34+
35+
.floating-menu {
36+
margin-top: 24px;
37+
left: 50%;
38+
bottom: 0;
39+
}
40+
}
41+
}
42+
</style>
43+
44+
<script lang="ts">
45+
import { defineComponent, PropType } from "vue";
46+
47+
import { RGBA } from "@/dispatcher/js-messages";
48+
49+
import LayoutRow from "@/components/layout/LayoutRow.vue";
50+
import ColorPicker from "@/components/widgets/floating-menus/ColorPicker.vue";
51+
import FloatingMenu from "@/components/widgets/floating-menus/FloatingMenu.vue";
52+
import TextInput from "@/components/widgets/inputs/TextInput.vue";
53+
import Separator from "@/components/widgets/separators/Separator.vue";
54+
55+
export default defineComponent({
56+
emits: ["update:value"],
57+
props: {
58+
value: { type: String as PropType<string>, required: true },
59+
label: { type: String as PropType<string>, required: false },
60+
disabled: { type: Boolean as PropType<boolean>, default: false },
61+
},
62+
computed: {
63+
color() {
64+
const r = parseInt(this.value.slice(0, 2), 16);
65+
const g = parseInt(this.value.slice(2, 4), 16);
66+
const b = parseInt(this.value.slice(4, 6), 16);
67+
const a = parseInt(this.value.slice(6, 8), 16);
68+
return { r, g, b, a: a / 255 };
69+
},
70+
},
71+
methods: {
72+
colorPickerUpdated(color: RGBA) {
73+
const twoDigitHex = (value: number): string => value.toString(16).padStart(2, "0");
74+
const alphaU8Scale = Math.floor(color.a * 255);
75+
const newValue = `${twoDigitHex(color.r)}${twoDigitHex(color.g)}${twoDigitHex(color.b)}${twoDigitHex(alphaU8Scale)}`;
76+
this.$emit("update:value", newValue);
77+
},
78+
textInputUpdated(newValue: string) {
79+
if ((newValue.length !== 6 && newValue.length !== 8) || !newValue.match(/[A-F,a-f,0-9]*/)) return;
80+
81+
this.$emit("update:value", newValue);
82+
},
83+
menuOpen() {
84+
(this.$refs.colorFloatingMenu as typeof FloatingMenu).setOpen();
85+
},
86+
},
87+
components: {
88+
TextInput,
89+
ColorPicker,
90+
LayoutRow,
91+
FloatingMenu,
92+
Separator,
93+
},
94+
});
95+
</script>

frontend/src/dispatcher/js-messages.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,7 @@ export function isWidgetSection(layoutRow: WidgetRow | WidgetSection): layoutRow
412412
return Boolean((layoutRow as WidgetSection).layout);
413413
}
414414

415-
export type WidgetKind = "NumberInput" | "Separator" | "IconButton" | "PopoverButton" | "OptionalInput" | "RadioInput" | "TextInput" | "TextLabel" | "IconLabel";
415+
export type WidgetKind = "NumberInput" | "Separator" | "IconButton" | "PopoverButton" | "OptionalInput" | "RadioInput" | "TextInput" | "TextLabel" | "IconLabel" | "ColorInput";
416416

417417
export interface Widget {
418418
kind: WidgetKind;

0 commit comments

Comments
 (0)