mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-29 11:58:11 +08:00
Restructure the entire editor codebase to consistently match the message hierarchy
Closes #744
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
use super::utility_types::misc::LayoutTarget;
|
||||
use crate::messages::layout::utility_types::layout_widget::Layout;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[remain::sorted]
|
||||
#[impl_message(Message, Layout)]
|
||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum LayoutMessage {
|
||||
RefreshLayout { layout_target: LayoutTarget },
|
||||
SendLayout { layout: Layout, layout_target: LayoutTarget },
|
||||
UpdateLayout { layout_target: LayoutTarget, widget_id: u64, value: serde_json::Value },
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
use super::utility_types::misc::LayoutTarget;
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::Key;
|
||||
use crate::messages::layout::utility_types::layout_widget::Layout;
|
||||
use crate::messages::layout::utility_types::layout_widget::Widget;
|
||||
use crate::messages::portfolio::document::utility_types::misc::KeyboardPlatformLayout;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
use graphene::layers::text_layer::Font;
|
||||
|
||||
use serde_json::Value;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct LayoutMessageHandler {
|
||||
layouts: [Layout; LayoutTarget::LayoutTargetLength as usize],
|
||||
}
|
||||
|
||||
impl<F: Fn(&MessageDiscriminant) -> Vec<Vec<Key>>> MessageHandler<LayoutMessage, (F, KeyboardPlatformLayout)> for LayoutMessageHandler {
|
||||
#[remain::check]
|
||||
fn process_message(&mut self, message: LayoutMessage, data: (F, KeyboardPlatformLayout), responses: &mut std::collections::VecDeque<Message>) {
|
||||
let (action_input_mapping, keyboard_platform) = data;
|
||||
|
||||
use LayoutMessage::*;
|
||||
#[remain::sorted]
|
||||
match message {
|
||||
RefreshLayout { layout_target } => {
|
||||
self.send_layout(layout_target, responses, &action_input_mapping, keyboard_platform);
|
||||
}
|
||||
SendLayout { layout, layout_target } => {
|
||||
self.layouts[layout_target as usize] = layout;
|
||||
|
||||
self.send_layout(layout_target, responses, &action_input_mapping, keyboard_platform);
|
||||
}
|
||||
UpdateLayout { layout_target, widget_id, value } => {
|
||||
let layout = &mut self.layouts[layout_target as usize];
|
||||
let widget_holder = layout.iter_mut().find(|widget| widget.widget_id == widget_id);
|
||||
if widget_holder.is_none() {
|
||||
log::trace!(
|
||||
"Could not find widget_id:{} on layout_target:{:?}. This could be an indication of a problem or just a user clicking off of an actively edited layer",
|
||||
widget_id,
|
||||
layout_target
|
||||
);
|
||||
return;
|
||||
}
|
||||
#[remain::sorted]
|
||||
match &mut widget_holder.unwrap().widget {
|
||||
Widget::CheckboxInput(checkbox_input) => {
|
||||
let update_value = value.as_bool().expect("CheckboxInput update was not of type: bool");
|
||||
checkbox_input.checked = update_value;
|
||||
let callback_message = (checkbox_input.on_update.callback)(checkbox_input);
|
||||
responses.push_back(callback_message);
|
||||
}
|
||||
Widget::ColorInput(color_input) => {
|
||||
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);
|
||||
}
|
||||
Widget::DropdownInput(dropdown_input) => {
|
||||
let update_value = value.as_u64().expect("DropdownInput update was not of type: u64");
|
||||
dropdown_input.selected_index = Some(update_value as u32);
|
||||
let callback_message = (dropdown_input.entries.iter().flatten().nth(update_value as usize).unwrap().on_update.callback)(&());
|
||||
responses.push_back(callback_message);
|
||||
}
|
||||
Widget::FontInput(font_input) => {
|
||||
let update_value = value.as_object().expect("FontInput update was not of type: object");
|
||||
let font_family_value = update_value.get("fontFamily").expect("FontInput update does not have a fontFamily");
|
||||
let font_style_value = update_value.get("fontStyle").expect("FontInput update does not have a fontStyle");
|
||||
|
||||
let font_family = font_family_value.as_str().expect("FontInput update fontFamily was not of type: string");
|
||||
let font_style = font_style_value.as_str().expect("FontInput update fontStyle was not of type: string");
|
||||
|
||||
font_input.font_family = font_family.into();
|
||||
font_input.font_style = font_style.into();
|
||||
|
||||
responses.push_back(
|
||||
PortfolioMessage::LoadFont {
|
||||
font: Font::new(font_family.into(), font_style.into()),
|
||||
is_default: false,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
let callback_message = (font_input.on_update.callback)(font_input);
|
||||
responses.push_back(callback_message);
|
||||
}
|
||||
Widget::IconButton(icon_button) => {
|
||||
let callback_message = (icon_button.on_update.callback)(icon_button);
|
||||
responses.push_back(callback_message);
|
||||
}
|
||||
Widget::IconLabel(_) => {}
|
||||
Widget::InvisibleStandinInput(invisible) => {
|
||||
let callback_message = (invisible.on_update.callback)(&());
|
||||
responses.push_back(callback_message);
|
||||
}
|
||||
Widget::NumberInput(number_input) => match value {
|
||||
Value::Number(num) => {
|
||||
let update_value = num.as_f64().unwrap();
|
||||
number_input.value = Some(update_value);
|
||||
let callback_message = (number_input.on_update.callback)(number_input);
|
||||
responses.push_back(callback_message);
|
||||
}
|
||||
Value::String(str) => match str.as_str() {
|
||||
"Increment" => responses.push_back((number_input.increment_callback_increase.callback)(number_input)),
|
||||
"Decrement" => responses.push_back((number_input.increment_callback_decrease.callback)(number_input)),
|
||||
_ => {
|
||||
panic!("Invalid string found when updating `NumberInput`")
|
||||
}
|
||||
},
|
||||
_ => panic!("Invalid type found when updating `NumberInput`"),
|
||||
},
|
||||
Widget::OptionalInput(optional_input) => {
|
||||
let update_value = value.as_bool().expect("OptionalInput update was not of type: bool");
|
||||
optional_input.checked = update_value;
|
||||
let callback_message = (optional_input.on_update.callback)(optional_input);
|
||||
responses.push_back(callback_message);
|
||||
}
|
||||
Widget::PopoverButton(_) => {}
|
||||
Widget::RadioInput(radio_input) => {
|
||||
let update_value = value.as_u64().expect("RadioInput update was not of type: u64");
|
||||
radio_input.selected_index = update_value as u32;
|
||||
let callback_message = (radio_input.entries[update_value as usize].on_update.callback)(&());
|
||||
responses.push_back(callback_message);
|
||||
}
|
||||
Widget::Separator(_) => {}
|
||||
Widget::SwatchPairInput(_) => {}
|
||||
Widget::TextAreaInput(text_area_input) => {
|
||||
let update_value = value.as_str().expect("TextAreaInput update was not of type: string");
|
||||
text_area_input.value = update_value.into();
|
||||
let callback_message = (text_area_input.on_update.callback)(text_area_input);
|
||||
responses.push_back(callback_message);
|
||||
}
|
||||
Widget::TextButton(text_button) => {
|
||||
let callback_message = (text_button.on_update.callback)(text_button);
|
||||
responses.push_back(callback_message);
|
||||
}
|
||||
Widget::TextInput(text_input) => {
|
||||
let update_value = value.as_str().expect("TextInput update was not of type: string");
|
||||
text_input.value = update_value.into();
|
||||
let callback_message = (text_input.on_update.callback)(text_input);
|
||||
responses.push_back(callback_message);
|
||||
}
|
||||
Widget::TextLabel(_) => {}
|
||||
};
|
||||
responses.push_back(RefreshLayout { layout_target }.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn actions(&self) -> ActionList {
|
||||
actions!()
|
||||
}
|
||||
}
|
||||
|
||||
impl LayoutMessageHandler {
|
||||
#[remain::check]
|
||||
fn send_layout(
|
||||
&self,
|
||||
layout_target: LayoutTarget,
|
||||
responses: &mut VecDeque<Message>,
|
||||
action_input_mapping: &impl Fn(&MessageDiscriminant) -> Vec<Vec<Key>>,
|
||||
keyboard_platform: KeyboardPlatformLayout,
|
||||
) {
|
||||
let layout = &self.layouts[layout_target as usize];
|
||||
#[remain::sorted]
|
||||
let message = match layout_target {
|
||||
LayoutTarget::DialogDetails => FrontendMessage::UpdateDialogDetails {
|
||||
layout_target,
|
||||
layout: layout.clone().unwrap_widget_layout(action_input_mapping, keyboard_platform).layout,
|
||||
},
|
||||
LayoutTarget::DocumentBar => FrontendMessage::UpdateDocumentBarLayout {
|
||||
layout_target,
|
||||
layout: layout.clone().unwrap_widget_layout(action_input_mapping, keyboard_platform).layout,
|
||||
},
|
||||
LayoutTarget::DocumentMode => FrontendMessage::UpdateDocumentModeLayout {
|
||||
layout_target,
|
||||
layout: layout.clone().unwrap_widget_layout(action_input_mapping, keyboard_platform).layout,
|
||||
},
|
||||
LayoutTarget::LayerTreeOptions => FrontendMessage::UpdateLayerTreeOptionsLayout {
|
||||
layout_target,
|
||||
layout: layout.clone().unwrap_widget_layout(action_input_mapping, keyboard_platform).layout,
|
||||
},
|
||||
LayoutTarget::MenuBar => FrontendMessage::UpdateMenuBarLayout {
|
||||
layout_target,
|
||||
layout: layout.clone().unwrap_menu_layout(action_input_mapping, keyboard_platform).layout,
|
||||
},
|
||||
LayoutTarget::PropertiesOptions => FrontendMessage::UpdatePropertyPanelOptionsLayout {
|
||||
layout_target,
|
||||
layout: layout.clone().unwrap_widget_layout(action_input_mapping, keyboard_platform).layout,
|
||||
},
|
||||
LayoutTarget::PropertiesSections => FrontendMessage::UpdatePropertyPanelSectionsLayout {
|
||||
layout_target,
|
||||
layout: layout.clone().unwrap_widget_layout(action_input_mapping, keyboard_platform).layout,
|
||||
},
|
||||
LayoutTarget::ToolOptions => FrontendMessage::UpdateToolOptionsLayout {
|
||||
layout_target,
|
||||
layout: layout.clone().unwrap_widget_layout(action_input_mapping, keyboard_platform).layout,
|
||||
},
|
||||
LayoutTarget::ToolShelf => FrontendMessage::UpdateToolShelfLayout {
|
||||
layout_target,
|
||||
layout: layout.clone().unwrap_widget_layout(action_input_mapping, keyboard_platform).layout,
|
||||
},
|
||||
LayoutTarget::WorkingColors => FrontendMessage::UpdateWorkingColorsLayout {
|
||||
layout_target,
|
||||
layout: layout.clone().unwrap_widget_layout(action_input_mapping, keyboard_platform).layout,
|
||||
},
|
||||
|
||||
#[remain::unsorted]
|
||||
LayoutTarget::LayoutTargetLength => panic!("`LayoutTargetLength` is not a valid Layout Target and is used for array indexing"),
|
||||
};
|
||||
responses.push_back(message.into());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
mod layout_message;
|
||||
mod layout_message_handler;
|
||||
|
||||
pub mod utility_types;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use layout_message::{LayoutMessage, LayoutMessageDiscriminant};
|
||||
#[doc(inline)]
|
||||
pub use layout_message_handler::LayoutMessageHandler;
|
||||
@@ -0,0 +1,292 @@
|
||||
use super::widgets::button_widgets::*;
|
||||
use super::widgets::input_widgets::*;
|
||||
use super::widgets::label_widgets::*;
|
||||
use super::widgets::menu_widgets::MenuLayout;
|
||||
use crate::application::generate_uuid;
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::Key;
|
||||
use crate::messages::input_mapper::utility_types::misc::{keys_text_shortcut, ActionKeys};
|
||||
use crate::messages::layout::utility_types::misc::LayoutTarget;
|
||||
use crate::messages::layout::LayoutMessage;
|
||||
use crate::messages::portfolio::document::utility_types::misc::KeyboardPlatformLayout;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::VecDeque;
|
||||
use std::rc::Rc;
|
||||
|
||||
pub trait PropertyHolder {
|
||||
fn properties(&self) -> Layout {
|
||||
Layout::WidgetLayout(WidgetLayout::default())
|
||||
}
|
||||
|
||||
fn register_properties(&self, responses: &mut VecDeque<Message>, layout_target: LayoutTarget) {
|
||||
responses.push_back(
|
||||
LayoutMessage::SendLayout {
|
||||
layout: self.properties(),
|
||||
layout_target,
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum Layout {
|
||||
WidgetLayout(WidgetLayout),
|
||||
MenuLayout(MenuLayout),
|
||||
}
|
||||
|
||||
impl Layout {
|
||||
pub fn unwrap_widget_layout(self, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Vec<Vec<Key>>, keyboard_platform: KeyboardPlatformLayout) -> WidgetLayout {
|
||||
if let Layout::WidgetLayout(mut widget_layout) = self {
|
||||
// Function used multiple times later in this code block to convert `ActionKeys::Action` to `ActionKeys::Keys` and append its shortcut to the tooltip
|
||||
let apply_shortcut_to_tooltip = |tooltip_shortcut: &mut ActionKeys, tooltip: &mut String| {
|
||||
tooltip_shortcut.to_keys(action_input_mapping);
|
||||
|
||||
if let ActionKeys::Keys(keys) = tooltip_shortcut {
|
||||
let shortcut_text = keys_text_shortcut(keys, keyboard_platform);
|
||||
|
||||
if !shortcut_text.is_empty() {
|
||||
if !tooltip.is_empty() {
|
||||
tooltip.push(' ');
|
||||
}
|
||||
tooltip.push('(');
|
||||
tooltip.push_str(&shortcut_text);
|
||||
tooltip.push(')');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Go through each widget to convert `ActionKeys::Action` to `ActionKeys::Keys` and append the key combination to the widget tooltip
|
||||
for widget_holder in &mut widget_layout.iter_mut() {
|
||||
// Handle all the widgets that have tooltips
|
||||
let mut tooltip_shortcut = match &mut widget_holder.widget {
|
||||
Widget::CheckboxInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
|
||||
Widget::ColorInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
|
||||
Widget::IconButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
|
||||
Widget::OptionalInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
|
||||
_ => None,
|
||||
};
|
||||
if let Some((tooltip, Some(tooltip_shortcut))) = &mut tooltip_shortcut {
|
||||
apply_shortcut_to_tooltip(tooltip_shortcut, tooltip);
|
||||
}
|
||||
|
||||
// Handle RadioInput separately because its tooltips are children of the widget
|
||||
if let Widget::RadioInput(radio_input) = &mut widget_holder.widget {
|
||||
for radio_entry_data in &mut radio_input.entries {
|
||||
if let RadioEntryData {
|
||||
tooltip,
|
||||
tooltip_shortcut: Some(tooltip_shortcut),
|
||||
..
|
||||
} = radio_entry_data
|
||||
{
|
||||
apply_shortcut_to_tooltip(tooltip_shortcut, tooltip);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
widget_layout
|
||||
} else {
|
||||
panic!("Tried to unwrap layout as WidgetLayout. Got {:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unwrap_menu_layout(self, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Vec<Vec<Key>>, _keyboard_platform: KeyboardPlatformLayout) -> MenuLayout {
|
||||
if let Layout::MenuLayout(mut menu_layout) = self {
|
||||
for menu_column in &mut menu_layout.layout {
|
||||
menu_column.children.fill_in_shortcut_actions_with_keys(action_input_mapping);
|
||||
}
|
||||
|
||||
menu_layout
|
||||
} else {
|
||||
panic!("Tried to unwrap layout as MenuLayout. Got {:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> Box<dyn Iterator<Item = &WidgetHolder> + '_> {
|
||||
match self {
|
||||
Layout::MenuLayout(menu_layout) => Box::new(menu_layout.iter()),
|
||||
Layout::WidgetLayout(widget_layout) => Box::new(widget_layout.iter()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iter_mut(&mut self) -> Box<dyn Iterator<Item = &mut WidgetHolder> + '_> {
|
||||
match self {
|
||||
Layout::MenuLayout(menu_layout) => Box::new(menu_layout.iter_mut()),
|
||||
Layout::WidgetLayout(widget_layout) => Box::new(widget_layout.iter_mut()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Layout {
|
||||
fn default() -> Self {
|
||||
Layout::WidgetLayout(WidgetLayout::default())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct WidgetLayout {
|
||||
pub layout: SubLayout,
|
||||
}
|
||||
|
||||
impl WidgetLayout {
|
||||
pub fn new(layout: SubLayout) -> Self {
|
||||
Self { layout }
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> WidgetIter<'_> {
|
||||
WidgetIter {
|
||||
stack: self.layout.iter().collect(),
|
||||
current_slice: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iter_mut(&mut self) -> WidgetIterMut<'_> {
|
||||
WidgetIterMut {
|
||||
stack: self.layout.iter_mut().collect(),
|
||||
current_slice: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct WidgetIter<'a> {
|
||||
pub stack: Vec<&'a LayoutGroup>,
|
||||
pub current_slice: Option<&'a [WidgetHolder]>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for WidgetIter<'a> {
|
||||
type Item = &'a WidgetHolder;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if let Some(item) = self.current_slice.and_then(|slice| slice.first()) {
|
||||
self.current_slice = Some(&self.current_slice.unwrap()[1..]);
|
||||
return Some(item);
|
||||
}
|
||||
|
||||
match self.stack.pop() {
|
||||
Some(LayoutGroup::Column { widgets }) => {
|
||||
self.current_slice = Some(widgets);
|
||||
self.next()
|
||||
}
|
||||
Some(LayoutGroup::Row { widgets }) => {
|
||||
self.current_slice = Some(widgets);
|
||||
self.next()
|
||||
}
|
||||
Some(LayoutGroup::Section { name: _, layout }) => {
|
||||
for layout_row in layout {
|
||||
self.stack.push(layout_row);
|
||||
}
|
||||
self.next()
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct WidgetIterMut<'a> {
|
||||
pub stack: Vec<&'a mut LayoutGroup>,
|
||||
pub current_slice: Option<&'a mut [WidgetHolder]>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for WidgetIterMut<'a> {
|
||||
type Item = &'a mut WidgetHolder;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if let Some((first, rest)) = self.current_slice.take().and_then(|slice| slice.split_first_mut()) {
|
||||
self.current_slice = Some(rest);
|
||||
return Some(first);
|
||||
};
|
||||
|
||||
match self.stack.pop() {
|
||||
Some(LayoutGroup::Column { widgets }) => {
|
||||
self.current_slice = Some(widgets);
|
||||
self.next()
|
||||
}
|
||||
Some(LayoutGroup::Row { widgets }) => {
|
||||
self.current_slice = Some(widgets);
|
||||
self.next()
|
||||
}
|
||||
Some(LayoutGroup::Section { name: _, layout }) => {
|
||||
for layout_row in layout {
|
||||
self.stack.push(layout_row);
|
||||
}
|
||||
self.next()
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type SubLayout = Vec<LayoutGroup>;
|
||||
|
||||
#[remain::sorted]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum LayoutGroup {
|
||||
#[serde(rename = "column")]
|
||||
Column {
|
||||
#[serde(rename = "columnWidgets")]
|
||||
widgets: Vec<WidgetHolder>,
|
||||
},
|
||||
#[serde(rename = "row")]
|
||||
Row {
|
||||
#[serde(rename = "rowWidgets")]
|
||||
widgets: Vec<WidgetHolder>,
|
||||
},
|
||||
#[serde(rename = "section")]
|
||||
Section { name: String, layout: SubLayout },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct WidgetHolder {
|
||||
#[serde(rename = "widgetId")]
|
||||
pub widget_id: u64,
|
||||
pub widget: Widget,
|
||||
}
|
||||
|
||||
impl WidgetHolder {
|
||||
pub fn new(widget: Widget) -> Self {
|
||||
Self { widget_id: generate_uuid(), widget }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct WidgetCallback<T> {
|
||||
pub callback: Rc<dyn Fn(&T) -> Message + 'static>,
|
||||
}
|
||||
|
||||
impl<T> WidgetCallback<T> {
|
||||
pub fn new(callback: impl Fn(&T) -> Message + 'static) -> Self {
|
||||
Self { callback: Rc::new(callback) }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Default for WidgetCallback<T> {
|
||||
fn default() -> Self {
|
||||
Self::new(|_| Message::NoOp)
|
||||
}
|
||||
}
|
||||
|
||||
#[remain::sorted]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum Widget {
|
||||
CheckboxInput(CheckboxInput),
|
||||
ColorInput(ColorInput),
|
||||
DropdownInput(DropdownInput),
|
||||
FontInput(FontInput),
|
||||
IconButton(IconButton),
|
||||
IconLabel(IconLabel),
|
||||
InvisibleStandinInput(InvisibleStandinInput),
|
||||
NumberInput(NumberInput),
|
||||
OptionalInput(OptionalInput),
|
||||
PopoverButton(PopoverButton),
|
||||
RadioInput(RadioInput),
|
||||
Separator(Separator),
|
||||
SwatchPairInput(SwatchPairInput),
|
||||
TextAreaInput(TextAreaInput),
|
||||
TextButton(TextButton),
|
||||
TextInput(TextInput),
|
||||
TextLabel(TextLabel),
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[remain::sorted]
|
||||
#[derive(PartialEq, Clone, Debug, Hash, Eq, Copy, Serialize, Deserialize)]
|
||||
#[repr(u8)]
|
||||
pub enum LayoutTarget {
|
||||
DialogDetails,
|
||||
DocumentBar,
|
||||
DocumentMode,
|
||||
LayerTreeOptions,
|
||||
MenuBar,
|
||||
PropertiesOptions,
|
||||
PropertiesSections,
|
||||
ToolOptions,
|
||||
ToolShelf,
|
||||
WorkingColors,
|
||||
|
||||
// KEEP THIS ENUM LAST
|
||||
// This is a marker that is used to define an array that is used to hold widgets
|
||||
#[remain::unsorted]
|
||||
LayoutTargetLength,
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod layout_widget;
|
||||
pub mod misc;
|
||||
pub mod widgets;
|
||||
@@ -0,0 +1,55 @@
|
||||
use crate::messages::input_mapper::utility_types::misc::ActionKeys;
|
||||
use crate::messages::layout::utility_types::layout_widget::WidgetCallback;
|
||||
|
||||
use derivative::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Default, Derivative, Serialize, Deserialize)]
|
||||
#[derivative(Debug, PartialEq)]
|
||||
pub struct IconButton {
|
||||
pub icon: String,
|
||||
|
||||
pub size: u32, // TODO: Convert to an `IconSize` enum
|
||||
|
||||
pub active: bool,
|
||||
|
||||
pub tooltip: String,
|
||||
|
||||
#[serde(skip)]
|
||||
pub tooltip_shortcut: Option<ActionKeys>,
|
||||
|
||||
// Callbacks
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
pub on_update: WidgetCallback<IconButton>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative)]
|
||||
#[derivative(Debug, PartialEq, Default)]
|
||||
pub struct PopoverButton {
|
||||
pub icon: Option<String>,
|
||||
|
||||
// Body
|
||||
pub header: String,
|
||||
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative, Default)]
|
||||
#[derivative(Debug, PartialEq)]
|
||||
#[serde(rename_all(serialize = "camelCase", deserialize = "camelCase"))]
|
||||
pub struct TextButton {
|
||||
pub label: String,
|
||||
|
||||
pub emphasized: bool,
|
||||
|
||||
#[serde(rename = "minWidth")]
|
||||
pub min_width: u32,
|
||||
|
||||
pub disabled: bool,
|
||||
|
||||
// Callbacks
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
pub on_update: WidgetCallback<TextButton>,
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
use crate::messages::input_mapper::utility_types::misc::ActionKeys;
|
||||
use crate::messages::layout::utility_types::layout_widget::WidgetCallback;
|
||||
|
||||
use graphene::color::Color;
|
||||
|
||||
use derivative::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Default, Derivative, Serialize, Deserialize)]
|
||||
#[derivative(Debug, PartialEq)]
|
||||
pub struct CheckboxInput {
|
||||
pub checked: bool,
|
||||
|
||||
pub icon: String,
|
||||
|
||||
pub tooltip: String,
|
||||
|
||||
#[serde(skip)]
|
||||
pub tooltip_shortcut: Option<ActionKeys>,
|
||||
|
||||
// Callbacks
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
pub on_update: WidgetCallback<CheckboxInput>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Derivative, Serialize, Deserialize)]
|
||||
#[derivative(Debug, PartialEq, Default)]
|
||||
pub struct ColorInput {
|
||||
pub value: Option<String>,
|
||||
|
||||
pub label: Option<String>,
|
||||
|
||||
#[serde(rename = "noTransparency")]
|
||||
#[derivative(Default(value = "true"))]
|
||||
pub no_transparency: bool,
|
||||
|
||||
pub disabled: bool,
|
||||
|
||||
pub tooltip: String,
|
||||
|
||||
#[serde(skip)]
|
||||
pub tooltip_shortcut: Option<ActionKeys>,
|
||||
|
||||
// Callbacks
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
pub on_update: WidgetCallback<ColorInput>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative)]
|
||||
#[derivative(Debug, PartialEq, Default)]
|
||||
pub struct DropdownInput {
|
||||
pub entries: DropdownInputEntries,
|
||||
|
||||
// This uses `u32` instead of `usize` since it will be serialized as a normal JS number (replace this with `usize` after switching to a Rust-based GUI)
|
||||
#[serde(rename = "selectedIndex")]
|
||||
pub selected_index: Option<u32>,
|
||||
|
||||
#[serde(rename = "drawIcon")]
|
||||
pub draw_icon: bool,
|
||||
|
||||
#[derivative(Default(value = "true"))]
|
||||
pub interactive: bool,
|
||||
|
||||
pub disabled: bool,
|
||||
//
|
||||
// Callbacks
|
||||
// `on_update` exists on the `DropdownEntryData`, not this parent `DropdownInput`
|
||||
}
|
||||
|
||||
pub type DropdownInputEntries = Vec<Vec<DropdownEntryData>>;
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative, Default)]
|
||||
#[derivative(Debug, PartialEq)]
|
||||
pub struct DropdownEntryData {
|
||||
pub value: String,
|
||||
|
||||
pub label: String,
|
||||
|
||||
pub icon: String,
|
||||
|
||||
pub shortcut: Vec<String>,
|
||||
|
||||
#[serde(rename = "shortcutRequiresLock")]
|
||||
pub shortcut_requires_lock: bool,
|
||||
|
||||
pub disabled: bool,
|
||||
|
||||
pub children: DropdownInputEntries,
|
||||
|
||||
// Callbacks
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
pub on_update: WidgetCallback<()>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative)]
|
||||
#[derivative(Debug, PartialEq, Default)]
|
||||
pub struct FontInput {
|
||||
#[serde(rename = "fontFamily")]
|
||||
pub font_family: String,
|
||||
|
||||
#[serde(rename = "fontStyle")]
|
||||
pub font_style: String,
|
||||
|
||||
#[serde(rename = "isStyle")]
|
||||
pub is_style_picker: bool,
|
||||
|
||||
pub disabled: bool,
|
||||
|
||||
// Callbacks
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
pub on_update: WidgetCallback<FontInput>,
|
||||
}
|
||||
|
||||
/// This widget allows for the flexible use of the layout system.
|
||||
/// In a custom layout, one can define a widget that is just used to trigger code on the backend.
|
||||
/// This is used in MenuLayout to pipe the triggering of messages from the frontend to backend.
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative, Default)]
|
||||
#[derivative(Debug, PartialEq)]
|
||||
pub struct InvisibleStandinInput {
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
pub on_update: WidgetCallback<()>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative)]
|
||||
#[derivative(Debug, PartialEq, Default)]
|
||||
pub struct NumberInput {
|
||||
pub label: String,
|
||||
|
||||
pub value: Option<f64>,
|
||||
|
||||
pub min: Option<f64>,
|
||||
|
||||
pub max: Option<f64>,
|
||||
|
||||
#[serde(rename = "isInteger")]
|
||||
pub is_integer: bool,
|
||||
|
||||
#[serde(rename = "displayDecimalPlaces")]
|
||||
#[derivative(Default(value = "3"))]
|
||||
pub display_decimal_places: u32,
|
||||
|
||||
pub unit: String,
|
||||
|
||||
#[serde(rename = "unitIsHiddenWhenEditing")]
|
||||
#[derivative(Default(value = "true"))]
|
||||
pub unit_is_hidden_when_editing: bool,
|
||||
|
||||
#[serde(rename = "incrementBehavior")]
|
||||
pub increment_behavior: NumberInputIncrementBehavior,
|
||||
|
||||
#[serde(rename = "incrementFactor")]
|
||||
#[derivative(Default(value = "1."))]
|
||||
pub increment_factor: f64,
|
||||
|
||||
pub disabled: bool,
|
||||
|
||||
// Callbacks
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
pub on_update: WidgetCallback<NumberInput>,
|
||||
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
pub increment_callback_increase: WidgetCallback<NumberInput>,
|
||||
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
pub increment_callback_decrease: WidgetCallback<NumberInput>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, Default, PartialEq, Eq)]
|
||||
pub enum NumberInputIncrementBehavior {
|
||||
#[default]
|
||||
Add,
|
||||
Multiply,
|
||||
Callback,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Derivative, Serialize, Deserialize)]
|
||||
#[derivative(Debug, PartialEq)]
|
||||
pub struct OptionalInput {
|
||||
pub checked: bool,
|
||||
|
||||
pub icon: String,
|
||||
|
||||
pub tooltip: String,
|
||||
|
||||
#[serde(skip)]
|
||||
pub tooltip_shortcut: Option<ActionKeys>,
|
||||
|
||||
// Callbacks
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
pub on_update: WidgetCallback<OptionalInput>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Derivative, Serialize, Deserialize)]
|
||||
#[derivative(Debug, PartialEq)]
|
||||
pub struct RadioInput {
|
||||
pub entries: Vec<RadioEntryData>,
|
||||
|
||||
// This uses `u32` instead of `usize` since it will be serialized as a normal JS number (replace this with `usize` after switching to a Rust-based GUI)
|
||||
#[serde(rename = "selectedIndex")]
|
||||
pub selected_index: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Derivative, Serialize, Deserialize)]
|
||||
#[derivative(Debug, PartialEq)]
|
||||
pub struct RadioEntryData {
|
||||
pub value: String,
|
||||
|
||||
pub label: String,
|
||||
|
||||
pub icon: String,
|
||||
|
||||
pub tooltip: String,
|
||||
|
||||
#[serde(skip)]
|
||||
pub tooltip_shortcut: Option<ActionKeys>,
|
||||
|
||||
// Callbacks
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
pub on_update: WidgetCallback<()>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative)]
|
||||
#[derivative(Debug, PartialEq, Default)]
|
||||
pub struct SwatchPairInput {
|
||||
pub primary: Color,
|
||||
|
||||
pub secondary: Color,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative)]
|
||||
#[derivative(Debug, PartialEq, Default)]
|
||||
pub struct TextAreaInput {
|
||||
pub value: String,
|
||||
|
||||
pub label: Option<String>,
|
||||
|
||||
pub disabled: bool,
|
||||
|
||||
// Callbacks
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
pub on_update: WidgetCallback<TextAreaInput>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative)]
|
||||
#[derivative(Debug, PartialEq, Default)]
|
||||
pub struct TextInput {
|
||||
pub value: String,
|
||||
|
||||
pub label: Option<String>,
|
||||
|
||||
pub disabled: bool,
|
||||
|
||||
// Callbacks
|
||||
#[serde(skip)]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
pub on_update: WidgetCallback<TextInput>,
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
use derivative::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative, Debug, Default, PartialEq, Eq)]
|
||||
pub struct IconLabel {
|
||||
pub icon: String,
|
||||
|
||||
#[serde(rename = "iconStyle")]
|
||||
pub icon_style: IconStyle,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative, Debug, Default, PartialEq, Eq)]
|
||||
pub enum IconStyle {
|
||||
#[default]
|
||||
Normal,
|
||||
Node,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Separator {
|
||||
pub direction: SeparatorDirection,
|
||||
|
||||
#[serde(rename = "type")]
|
||||
pub separator_type: SeparatorType,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum SeparatorDirection {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum SeparatorType {
|
||||
Related,
|
||||
Unrelated,
|
||||
Section,
|
||||
List,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Derivative, Debug, PartialEq, Eq, Default)]
|
||||
pub struct TextLabel {
|
||||
pub bold: bool,
|
||||
|
||||
pub italic: bool,
|
||||
|
||||
#[serde(rename = "tableAlign")]
|
||||
pub table_align: bool,
|
||||
|
||||
pub multiline: bool,
|
||||
|
||||
// Body
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
// TODO: Add UserInputLabel
|
||||
@@ -0,0 +1,134 @@
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::Key;
|
||||
use crate::messages::input_mapper::utility_types::misc::ActionKeys;
|
||||
use crate::messages::layout::utility_types::layout_widget::WidgetHolder;
|
||||
use crate::messages::layout::utility_types::layout_widget::{Widget, WidgetCallback};
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::input_widgets::InvisibleStandinInput;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||
pub struct MenuEntryGroups(pub Vec<Vec<MenuEntry>>);
|
||||
|
||||
impl MenuEntryGroups {
|
||||
pub fn empty() -> Self {
|
||||
Self(Vec::new())
|
||||
}
|
||||
|
||||
pub fn fill_in_shortcut_actions_with_keys(&mut self, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Vec<Vec<Key>>) {
|
||||
let entries = self.0.iter_mut().flatten();
|
||||
|
||||
for entry in entries {
|
||||
if let Some(action_keys) = &mut entry.shortcut {
|
||||
action_keys.to_keys(action_input_mapping);
|
||||
}
|
||||
|
||||
// Recursively do this for the children also
|
||||
entry.children.fill_in_shortcut_actions_with_keys(action_input_mapping);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct MenuEntry {
|
||||
pub label: String,
|
||||
pub icon: Option<String>,
|
||||
pub children: MenuEntryGroups,
|
||||
pub action: WidgetHolder,
|
||||
pub shortcut: Option<ActionKeys>,
|
||||
}
|
||||
|
||||
impl MenuEntry {
|
||||
pub fn create_action(callback: impl Fn(&()) -> Message + 'static) -> WidgetHolder {
|
||||
WidgetHolder::new(Widget::InvisibleStandinInput(InvisibleStandinInput {
|
||||
on_update: WidgetCallback::new(callback),
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn no_action() -> WidgetHolder {
|
||||
MenuEntry::create_action(|_| Message::NoOp)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MenuEntry {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
action: MenuEntry::create_action(|_| DialogMessage::RequestComingSoonDialog { issue: None }.into()),
|
||||
label: "".into(),
|
||||
icon: None,
|
||||
children: MenuEntryGroups::empty(),
|
||||
shortcut: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct MenuColumn {
|
||||
pub label: String,
|
||||
pub children: MenuEntryGroups,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct MenuLayout {
|
||||
pub layout: Vec<MenuColumn>,
|
||||
}
|
||||
|
||||
impl MenuLayout {
|
||||
pub fn new(layout: Vec<MenuColumn>) -> Self {
|
||||
Self { layout }
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &WidgetHolder> + '_ {
|
||||
MenuLayoutIter {
|
||||
stack: self.layout.iter().flat_map(|column| column.children.0.iter()).flat_map(|group| group.iter()).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut WidgetHolder> + '_ {
|
||||
MenuLayoutIterMut {
|
||||
stack: self.layout.iter_mut().flat_map(|column| column.children.0.iter_mut()).flat_map(|group| group.iter_mut()).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct MenuLayoutIter<'a> {
|
||||
pub stack: Vec<&'a MenuEntry>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for MenuLayoutIter<'a> {
|
||||
type Item = &'a WidgetHolder;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match self.stack.pop() {
|
||||
Some(menu_entry) => {
|
||||
let more_entries = menu_entry.children.0.iter().flat_map(|entry| entry.iter());
|
||||
self.stack.extend(more_entries);
|
||||
|
||||
Some(&menu_entry.action)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MenuLayoutIterMut<'a> {
|
||||
pub stack: Vec<&'a mut MenuEntry>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for MenuLayoutIterMut<'a> {
|
||||
type Item = &'a mut WidgetHolder;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match self.stack.pop() {
|
||||
Some(menu_entry) => {
|
||||
let more_entries = menu_entry.children.0.iter_mut().flat_map(|entry| entry.iter_mut());
|
||||
self.stack.extend(more_entries);
|
||||
|
||||
Some(&mut menu_entry.action)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod button_widgets;
|
||||
pub mod input_widgets;
|
||||
pub mod label_widgets;
|
||||
pub mod menu_widgets;
|
||||
Reference in New Issue
Block a user