mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-29 19:58:11 +08:00
Color Input (#565)
* initial working prototype * clean up component * Fix alignment * Code review tweaks Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
committed by
Keavon Chambers
co-authored by
Keavon Chambers
parent
f718fa5af1
commit
1b1a6a2a12
@@ -2,7 +2,7 @@ use super::layer_panel::LayerDataTypeDiscriminant;
|
|||||||
use crate::document::properties_panel_message::TransformOp;
|
use crate::document::properties_panel_message::TransformOp;
|
||||||
use crate::layout::layout_message::LayoutTarget;
|
use crate::layout::layout_message::LayoutTarget;
|
||||||
use crate::layout::widgets::{
|
use crate::layout::widgets::{
|
||||||
IconLabel, LayoutRow, NumberInput, PopoverButton, Separator, SeparatorDirection, SeparatorType, TextInput, TextLabel, Widget, WidgetCallback, WidgetHolder, WidgetLayout,
|
ColorInput, IconLabel, LayoutRow, NumberInput, PopoverButton, Separator, SeparatorDirection, SeparatorType, TextInput, TextLabel, Widget, WidgetCallback, WidgetHolder, WidgetLayout,
|
||||||
};
|
};
|
||||||
use crate::message_prelude::*;
|
use crate::message_prelude::*;
|
||||||
|
|
||||||
@@ -424,9 +424,9 @@ fn node_section_fill(fill: &Fill) -> Option<LayoutRow> {
|
|||||||
separator_type: SeparatorType::Related,
|
separator_type: SeparatorType::Related,
|
||||||
direction: SeparatorDirection::Horizontal,
|
direction: SeparatorDirection::Horizontal,
|
||||||
})),
|
})),
|
||||||
WidgetHolder::new(Widget::TextInput(TextInput {
|
WidgetHolder::new(Widget::ColorInput(ColorInput {
|
||||||
value: color.rgba_hex(),
|
value: color.rgba_hex(),
|
||||||
on_update: WidgetCallback::new(|text_input: &TextInput| {
|
on_update: WidgetCallback::new(|text_input: &ColorInput| {
|
||||||
if let Some(color) = Color::from_rgba_str(&text_input.value).or(Color::from_rgb_str(&text_input.value)) {
|
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);
|
let new_fill = Fill::Solid(color);
|
||||||
PropertiesPanelMessage::ModifyFill { fill: new_fill }.into()
|
PropertiesPanelMessage::ModifyFill { fill: new_fill }.into()
|
||||||
@@ -455,9 +455,9 @@ fn node_section_fill(fill: &Fill) -> Option<LayoutRow> {
|
|||||||
separator_type: SeparatorType::Related,
|
separator_type: SeparatorType::Related,
|
||||||
direction: SeparatorDirection::Horizontal,
|
direction: SeparatorDirection::Horizontal,
|
||||||
})),
|
})),
|
||||||
WidgetHolder::new(Widget::TextInput(TextInput {
|
WidgetHolder::new(Widget::ColorInput(ColorInput {
|
||||||
value: gradient_1.positions[0].1.rgba_hex(),
|
value: gradient_1.positions[0].1.rgba_hex(),
|
||||||
on_update: WidgetCallback::new(move |text_input: &TextInput| {
|
on_update: WidgetCallback::new(move |text_input: &ColorInput| {
|
||||||
if let Some(color) = Color::from_rgba_str(&text_input.value).or(Color::from_rgb_str(&text_input.value)) {
|
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();
|
let mut new_gradient = (*gradient_1).clone();
|
||||||
new_gradient.positions[0].1 = color;
|
new_gradient.positions[0].1 = color;
|
||||||
@@ -483,9 +483,9 @@ fn node_section_fill(fill: &Fill) -> Option<LayoutRow> {
|
|||||||
separator_type: SeparatorType::Related,
|
separator_type: SeparatorType::Related,
|
||||||
direction: SeparatorDirection::Horizontal,
|
direction: SeparatorDirection::Horizontal,
|
||||||
})),
|
})),
|
||||||
WidgetHolder::new(Widget::TextInput(TextInput {
|
WidgetHolder::new(Widget::ColorInput(ColorInput {
|
||||||
value: gradient_2.positions[1].1.rgba_hex(),
|
value: gradient_2.positions[1].1.rgba_hex(),
|
||||||
on_update: WidgetCallback::new(move |text_input: &TextInput| {
|
on_update: WidgetCallback::new(move |text_input: &ColorInput| {
|
||||||
if let Some(color) = Color::from_rgba_str(&text_input.value).or(Color::from_rgb_str(&text_input.value)) {
|
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();
|
let mut new_gradient = (*gradient_2).clone();
|
||||||
new_gradient.positions[1].1 = color;
|
new_gradient.positions[1].1 = color;
|
||||||
@@ -524,9 +524,9 @@ fn node_section_stroke(stroke: &Stroke) -> LayoutRow {
|
|||||||
separator_type: SeparatorType::Related,
|
separator_type: SeparatorType::Related,
|
||||||
direction: SeparatorDirection::Horizontal,
|
direction: SeparatorDirection::Horizontal,
|
||||||
})),
|
})),
|
||||||
WidgetHolder::new(Widget::TextInput(TextInput {
|
WidgetHolder::new(Widget::ColorInput(ColorInput {
|
||||||
value: stroke.color().rgba_hex(),
|
value: stroke.color().rgba_hex(),
|
||||||
on_update: WidgetCallback::new(move |text_input: &TextInput| {
|
on_update: WidgetCallback::new(move |text_input: &ColorInput| {
|
||||||
PropertiesPanelMessage::ModifyStroke {
|
PropertiesPanelMessage::ModifyStroke {
|
||||||
color: text_input.value.clone(),
|
color: text_input.value.clone(),
|
||||||
weight: weight as f64,
|
weight: weight as f64,
|
||||||
|
|||||||
@@ -80,17 +80,23 @@ impl MessageHandler<LayoutMessage, ()> for LayoutMessageHandler {
|
|||||||
responses.push_back(callback_message);
|
responses.push_back(callback_message);
|
||||||
}
|
}
|
||||||
Widget::RadioInput(radio_input) => {
|
Widget::RadioInput(radio_input) => {
|
||||||
let update_value = value.as_u64().expect("OptionalInput update was not of type: u64");
|
let update_value = value.as_u64().expect("RadioInput update was not of type: u64");
|
||||||
radio_input.selected_index = update_value as u32;
|
radio_input.selected_index = update_value as u32;
|
||||||
let callback_message = (radio_input.entries[update_value as usize].on_update.callback)(&());
|
let callback_message = (radio_input.entries[update_value as usize].on_update.callback)(&());
|
||||||
responses.push_back(callback_message);
|
responses.push_back(callback_message);
|
||||||
}
|
}
|
||||||
Widget::TextInput(text_input) => {
|
Widget::TextInput(text_input) => {
|
||||||
let update_value = value.as_str().expect("OptionalInput update was not of type: string");
|
let update_value = value.as_str().expect("TextInput update was not of type: string");
|
||||||
text_input.value = update_value.into();
|
text_input.value = update_value.into();
|
||||||
let callback_message = (text_input.on_update.callback)(text_input);
|
let callback_message = (text_input.on_update.callback)(text_input);
|
||||||
responses.push_back(callback_message);
|
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 callback_message = (color_input.on_update.callback)(color_input);
|
||||||
|
responses.push_back(callback_message);
|
||||||
|
}
|
||||||
Widget::TextLabel(_) => {}
|
Widget::TextLabel(_) => {}
|
||||||
};
|
};
|
||||||
self.send_layout(layout_target, responses);
|
self.send_layout(layout_target, responses);
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ impl<T> Default for WidgetCallback<T> {
|
|||||||
#[remain::sorted]
|
#[remain::sorted]
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub enum Widget {
|
pub enum Widget {
|
||||||
|
ColorInput(ColorInput),
|
||||||
IconButton(IconButton),
|
IconButton(IconButton),
|
||||||
IconLabel(IconLabel),
|
IconLabel(IconLabel),
|
||||||
NumberInput(NumberInput),
|
NumberInput(NumberInput),
|
||||||
@@ -199,6 +200,15 @@ pub struct TextInput {
|
|||||||
pub on_update: WidgetCallback<TextInput>,
|
pub on_update: WidgetCallback<TextInput>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, Deserialize, Derivative)]
|
||||||
|
#[derivative(Debug, PartialEq, Default)]
|
||||||
|
pub struct ColorInput {
|
||||||
|
pub value: String,
|
||||||
|
#[serde(skip)]
|
||||||
|
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||||
|
pub on_update: WidgetCallback<ColorInput>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
|
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
|
||||||
pub enum NumberInputIncrementBehavior {
|
pub enum NumberInputIncrementBehavior {
|
||||||
Add,
|
Add,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
:incrementCallbackDecrease="() => updateLayout(component.widget_id, 'Decrement')"
|
:incrementCallbackDecrease="() => updateLayout(component.widget_id, 'Decrement')"
|
||||||
/>
|
/>
|
||||||
<TextInput v-if="component.kind === 'TextInput'" v-bind="component.props" @commitText="(value: string) => updateLayout(component.widget_id, value)" />
|
<TextInput v-if="component.kind === 'TextInput'" v-bind="component.props" @commitText="(value: string) => updateLayout(component.widget_id, value)" />
|
||||||
|
<ColorInput v-if="component.kind === 'ColorInput'" v-bind="component.props" @update:value="(value: string) => updateLayout(component.widget_id, value)" />
|
||||||
<IconButton v-if="component.kind === 'IconButton'" v-bind="component.props" :action="() => updateLayout(component.widget_id, null)" />
|
<IconButton v-if="component.kind === 'IconButton'" v-bind="component.props" :action="() => updateLayout(component.widget_id, null)" />
|
||||||
<OptionalInput v-if="component.kind === 'OptionalInput'" v-bind="component.props" @update:checked="(value: boolean) => updateLayout(component.widget_id, value)" />
|
<OptionalInput v-if="component.kind === 'OptionalInput'" v-bind="component.props" @update:checked="(value: boolean) => updateLayout(component.widget_id, value)" />
|
||||||
<RadioInput v-if="component.kind === 'RadioInput'" v-bind="component.props" @update:selectedIndex="(value: number) => updateLayout(component.widget_id, value)" />
|
<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";
|
|||||||
|
|
||||||
import IconButton from "@/components/widgets/buttons/IconButton.vue";
|
import IconButton from "@/components/widgets/buttons/IconButton.vue";
|
||||||
import PopoverButton from "@/components/widgets/buttons/PopoverButton.vue";
|
import PopoverButton from "@/components/widgets/buttons/PopoverButton.vue";
|
||||||
|
import ColorInput from "@/components/widgets/inputs/ColorInput.vue";
|
||||||
import NumberInput from "@/components/widgets/inputs/NumberInput.vue";
|
import NumberInput from "@/components/widgets/inputs/NumberInput.vue";
|
||||||
import OptionalInput from "@/components/widgets/inputs/OptionalInput.vue";
|
import OptionalInput from "@/components/widgets/inputs/OptionalInput.vue";
|
||||||
import RadioInput from "@/components/widgets/inputs/RadioInput.vue";
|
import RadioInput from "@/components/widgets/inputs/RadioInput.vue";
|
||||||
@@ -70,6 +72,7 @@ export default defineComponent({
|
|||||||
RadioInput,
|
RadioInput,
|
||||||
TextLabel,
|
TextLabel,
|
||||||
IconLabel,
|
IconLabel,
|
||||||
|
ColorInput,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
<template>
|
||||||
|
<LayoutRow class="color-input">
|
||||||
|
<TextInput :value="value" :label="label" :disabled="disabled" @commitText="(value: string) => textInputUpdated(value)" :center="true" />
|
||||||
|
<Separator :type="'Related'" />
|
||||||
|
<LayoutRow class="swatch">
|
||||||
|
<button class="swatch-button" @click="() => menuOpen()" :style="{ background: `#${value}` }"></button>
|
||||||
|
<FloatingMenu :type="'Popover'" :direction="'Bottom'" horizontal ref="colorFloatingMenu">
|
||||||
|
<ColorPicker @update:color="(color) => colorPickerUpdated(color)" :color="color" />
|
||||||
|
</FloatingMenu>
|
||||||
|
</LayoutRow>
|
||||||
|
</LayoutRow>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.color-input {
|
||||||
|
.text-input input {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swatch {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.swatch-button {
|
||||||
|
height: 24px;
|
||||||
|
width: 24px;
|
||||||
|
bottom: 0;
|
||||||
|
left: 50%;
|
||||||
|
padding: 0;
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-menu {
|
||||||
|
margin-top: 24px;
|
||||||
|
left: 50%;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, PropType } from "vue";
|
||||||
|
|
||||||
|
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 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 },
|
||||||
|
label: { type: String as PropType<string>, required: false },
|
||||||
|
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
color() {
|
||||||
|
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 };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
colorPickerUpdated(color: RGBA) {
|
||||||
|
const twoDigitHex = (value: number): string => value.toString(16).padStart(2, "0");
|
||||||
|
const alphaU8Scale = Math.floor(color.a * 255);
|
||||||
|
const newValue = `${twoDigitHex(color.r)}${twoDigitHex(color.g)}${twoDigitHex(color.b)}${twoDigitHex(alphaU8Scale)}`;
|
||||||
|
this.$emit("update:value", newValue);
|
||||||
|
},
|
||||||
|
textInputUpdated(newValue: string) {
|
||||||
|
if ((newValue.length !== 6 && newValue.length !== 8) || !newValue.match(/[A-F,a-f,0-9]*/)) return;
|
||||||
|
|
||||||
|
this.$emit("update:value", newValue);
|
||||||
|
},
|
||||||
|
menuOpen() {
|
||||||
|
(this.$refs.colorFloatingMenu as typeof FloatingMenu).setOpen();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
TextInput,
|
||||||
|
ColorPicker,
|
||||||
|
LayoutRow,
|
||||||
|
FloatingMenu,
|
||||||
|
Separator,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -412,7 +412,7 @@ export function isWidgetSection(layoutRow: WidgetRow | WidgetSection): layoutRow
|
|||||||
return Boolean((layoutRow as WidgetSection).layout);
|
return Boolean((layoutRow as WidgetSection).layout);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WidgetKind = "NumberInput" | "Separator" | "IconButton" | "PopoverButton" | "OptionalInput" | "RadioInput" | "TextInput" | "TextLabel" | "IconLabel";
|
export type WidgetKind = "NumberInput" | "Separator" | "IconButton" | "PopoverButton" | "OptionalInput" | "RadioInput" | "TextInput" | "TextLabel" | "IconLabel" | "ColorInput";
|
||||||
|
|
||||||
export interface Widget {
|
export interface Widget {
|
||||||
kind: WidgetKind;
|
kind: WidgetKind;
|
||||||
|
|||||||
Reference in New Issue
Block a user