diff --git a/editor/src/dispatcher.rs b/editor/src/dispatcher.rs index 8b4c3e9c..1ec85dd8 100644 --- a/editor/src/dispatcher.rs +++ b/editor/src/dispatcher.rs @@ -252,6 +252,7 @@ impl Dispatcher { #[cfg(test)] mod test { use crate::application::Editor; + use crate::messages::layout::utility_types::layout_widget::DiffUpdate; use crate::messages::portfolio::document::utility_types::clipboards::Clipboard; use crate::messages::prelude::*; use crate::test_utils::EditorTestUtils; @@ -570,10 +571,12 @@ mod test { for response in responses { // Check for the existence of the file format incompatibility warning dialog after opening the test file - if let FrontendMessage::UpdateDialogDetails { layout_target: _, layout } = response { - if let LayoutGroup::Row { widgets } = &layout[0] { - if let Widget::TextLabel(TextLabel { value, .. }) = &widgets[0].widget { - print_problem_to_terminal_on_failure(value); + if let FrontendMessage::UpdateDialogDetails { layout_target: _, diff } = response { + if let DiffUpdate::SubLayout(sub_layout) = &diff[0].new_value { + if let LayoutGroup::Row { widgets } = &sub_layout[0] { + if let Widget::TextLabel(TextLabel { value, .. }) = &widgets[0].widget { + print_problem_to_terminal_on_failure(value); + } } } } diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index 8246fd24..8f62eb50 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -1,5 +1,5 @@ use super::utility_types::{FrontendDocumentDetails, FrontendImageData, MouseCursorIcon}; -use crate::messages::layout::utility_types::layout_widget::SubLayout; +use crate::messages::layout::utility_types::layout_widget::WidgetDiff; use crate::messages::layout::utility_types::misc::LayoutTarget; use crate::messages::layout::utility_types::widgets::menu_widgets::MenuBarEntry; use crate::messages::portfolio::document::node_graph::{FrontendNode, FrontendNodeLink, FrontendNodeType}; @@ -143,7 +143,7 @@ pub enum FrontendMessage { UpdateDialogDetails { #[serde(rename = "layoutTarget")] layout_target: LayoutTarget, - layout: SubLayout, + diff: Vec, }, UpdateDocumentArtboards { svg: String, @@ -154,7 +154,7 @@ pub enum FrontendMessage { UpdateDocumentBarLayout { #[serde(rename = "layoutTarget")] layout_target: LayoutTarget, - layout: SubLayout, + diff: Vec, }, UpdateDocumentLayerDetails { data: LayerPanelEntry, @@ -170,7 +170,7 @@ pub enum FrontendMessage { UpdateDocumentModeLayout { #[serde(rename = "layoutTarget")] layout_target: LayoutTarget, - layout: SubLayout, + diff: Vec, }, UpdateDocumentOverlays { svg: String, @@ -208,7 +208,7 @@ pub enum FrontendMessage { UpdateLayerTreeOptionsLayout { #[serde(rename = "layoutTarget")] layout_target: LayoutTarget, - layout: SubLayout, + diff: Vec, }, UpdateMenuBarLayout { #[serde(rename = "layoutTarget")] @@ -225,7 +225,7 @@ pub enum FrontendMessage { UpdateNodeGraphBarLayout { #[serde(rename = "layoutTarget")] layout_target: LayoutTarget, - layout: SubLayout, + diff: Vec, }, UpdateNodeGraphSelection { selected: Vec, @@ -244,26 +244,26 @@ pub enum FrontendMessage { UpdatePropertyPanelOptionsLayout { #[serde(rename = "layoutTarget")] layout_target: LayoutTarget, - layout: SubLayout, + diff: Vec, }, UpdatePropertyPanelSectionsLayout { #[serde(rename = "layoutTarget")] layout_target: LayoutTarget, - layout: SubLayout, + diff: Vec, }, UpdateToolOptionsLayout { #[serde(rename = "layoutTarget")] layout_target: LayoutTarget, - layout: SubLayout, + diff: Vec, }, UpdateToolShelfLayout { #[serde(rename = "layoutTarget")] layout_target: LayoutTarget, - layout: SubLayout, + diff: Vec, }, UpdateWorkingColorsLayout { #[serde(rename = "layoutTarget")] layout_target: LayoutTarget, - layout: SubLayout, + diff: Vec, }, } diff --git a/editor/src/messages/layout/layout_message.rs b/editor/src/messages/layout/layout_message.rs index ca820cac..4d2430b4 100644 --- a/editor/src/messages/layout/layout_message.rs +++ b/editor/src/messages/layout/layout_message.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; #[impl_message(Message, Layout)] #[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] pub enum LayoutMessage { - RefreshLayout { layout_target: LayoutTarget }, + ResendActiveWidget { layout_target: LayoutTarget, dirty_id: u64 }, SendLayout { layout: Layout, layout_target: LayoutTarget }, UpdateLayout { layout_target: LayoutTarget, widget_id: u64, value: serde_json::Value }, } diff --git a/editor/src/messages/layout/layout_message_handler.rs b/editor/src/messages/layout/layout_message_handler.rs index 9a4b6e01..27518820 100644 --- a/editor/src/messages/layout/layout_message_handler.rs +++ b/editor/src/messages/layout/layout_message_handler.rs @@ -1,7 +1,8 @@ +use super::utility_types::layout_widget::{LayoutGroup, WidgetDiff, WidgetHolder}; use super::utility_types::misc::LayoutTarget; use crate::messages::input_mapper::utility_types::input_keyboard::KeysGroup; -use crate::messages::layout::utility_types::layout_widget::Layout; -use crate::messages::layout::utility_types::layout_widget::Widget; +use crate::messages::layout::utility_types::layout_widget::{DiffUpdate, Widget}; +use crate::messages::layout::utility_types::layout_widget::{Layout, WidgetLayout}; use crate::messages::prelude::*; use document_legacy::color::Color; @@ -16,6 +17,32 @@ pub struct LayoutMessageHandler { layouts: [Layout; LayoutTarget::LayoutTargetLength as usize], } +impl LayoutMessageHandler { + /// Get the widget path for the widget with the specified id + fn get_widget_path(widget_layout: &WidgetLayout, id: u64) -> Option<(&WidgetHolder, Vec)> { + let mut stack = widget_layout.layout.iter().enumerate().map(|(index, val)| (vec![index], val)).collect::>(); + while let Some((mut widget_path, group)) = stack.pop() { + match group { + // Check if any of the widgets in the current column or row have the correct id + LayoutGroup::Column { widgets } | LayoutGroup::Row { widgets } => { + for (index, widget) in widgets.iter().enumerate() { + // Return if this is the correct ID + if widget.widget_id == id { + widget_path.push(index); + return Some((widget, widget_path)); + } + } + } + // A section contains more LayoutGroups which we add to the stack. + LayoutGroup::Section { layout, .. } => { + stack.extend(layout.iter().enumerate().map(|(index, val)| ([widget_path.as_slice(), &[index]].concat(), val))); + } + } + } + None + } +} + impl Vec> MessageHandler for LayoutMessageHandler { #[remain::check] fn process_message(&mut self, message: LayoutMessage, data: F, responses: &mut std::collections::VecDeque) { @@ -24,14 +51,22 @@ impl Vec> MessageHandler { - self.send_layout(layout_target, responses, &action_input_mapping); - } - SendLayout { layout, layout_target } => { - self.layouts[layout_target as usize] = layout; - - self.send_layout(layout_target, responses, &action_input_mapping); + ResendActiveWidget { layout_target, dirty_id } => { + // Find the updated diff based on the specified layout target + let Some(diff) = (match &self.layouts[layout_target as usize] { + Layout::MenuLayout(_) => return, + Layout::WidgetLayout(layout) => Self::get_widget_path(layout, dirty_id).map(|(widget, widget_path)| { + // Create a widget update diff for the relevant id + let new_value = DiffUpdate::Widget(widget.clone()); + WidgetDiff { widget_path, new_value } + }), + }) else { + return; + }; + // Resend that diff + self.send_diff(vec![diff], layout_target, responses, &action_input_mapping); } + SendLayout { layout, layout_target } => self.send_layout(layout_target, layout, responses, &action_input_mapping), UpdateLayout { layout_target, widget_id, value } => { // Look up the layout let layout = if let Some(layout) = self.layouts.get_mut(layout_target as usize) { @@ -84,7 +119,7 @@ impl Vec> MessageHandler Vec> MessageHandler {} }; - responses.push_back(RefreshLayout { layout_target }.into()); + responses.push_back(ResendActiveWidget { layout_target, dirty_id: widget_id }.into()); } } } @@ -208,55 +243,57 @@ impl Vec> MessageHandler, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Vec) { + // We don't diff the menu bar layout yet. + if matches!(new_layout, Layout::MenuLayout(_)) { + // Skip update if the same + if self.layouts[layout_target as usize] == new_layout { + return; + } + // Update the backend storage + self.layouts[layout_target as usize] = new_layout; + // Update the UI + responses.push_back( + FrontendMessage::UpdateMenuBarLayout { + layout_target, + layout: self.layouts[layout_target as usize].clone().unwrap_menu_layout(action_input_mapping).layout, + } + .into(), + ); + return; + } + + let mut widget_diffs = Vec::new(); + self.layouts[layout_target as usize].diff(new_layout, &mut Vec::new(), &mut widget_diffs); + // Skip sending if no diff. + if widget_diffs.is_empty() { + return; + } + + self.send_diff(widget_diffs, layout_target, responses, action_input_mapping); + } + + /// Send a diff to the frontend based on the layout target. #[remain::check] - fn send_layout(&self, layout_target: LayoutTarget, responses: &mut VecDeque, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Vec) { - let layout = &self.layouts[layout_target as usize]; + fn send_diff(&self, mut diff: Vec, layout_target: LayoutTarget, responses: &mut VecDeque, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Vec) { + diff.iter_mut().for_each(|diff| diff.new_value.apply_shortcut(action_input_mapping)); + + trace!("{layout_target:?} diff {diff:#?}"); + #[remain::sorted] let message = match layout_target { - LayoutTarget::DialogDetails => FrontendMessage::UpdateDialogDetails { - layout_target, - layout: layout.clone().unwrap_widget_layout(action_input_mapping).layout, - }, - LayoutTarget::DocumentBar => FrontendMessage::UpdateDocumentBarLayout { - layout_target, - layout: layout.clone().unwrap_widget_layout(action_input_mapping).layout, - }, - LayoutTarget::DocumentMode => FrontendMessage::UpdateDocumentModeLayout { - layout_target, - layout: layout.clone().unwrap_widget_layout(action_input_mapping).layout, - }, - LayoutTarget::LayerTreeOptions => FrontendMessage::UpdateLayerTreeOptionsLayout { - layout_target, - layout: layout.clone().unwrap_widget_layout(action_input_mapping).layout, - }, - LayoutTarget::MenuBar => FrontendMessage::UpdateMenuBarLayout { - layout_target, - layout: layout.clone().unwrap_menu_layout(action_input_mapping).layout, - }, - LayoutTarget::NodeGraphBar => FrontendMessage::UpdateNodeGraphBarLayout { - layout_target, - layout: layout.clone().unwrap_widget_layout(action_input_mapping).layout, - }, - LayoutTarget::PropertiesOptions => FrontendMessage::UpdatePropertyPanelOptionsLayout { - layout_target, - layout: layout.clone().unwrap_widget_layout(action_input_mapping).layout, - }, - LayoutTarget::PropertiesSections => FrontendMessage::UpdatePropertyPanelSectionsLayout { - layout_target, - layout: layout.clone().unwrap_widget_layout(action_input_mapping).layout, - }, - LayoutTarget::ToolOptions => FrontendMessage::UpdateToolOptionsLayout { - layout_target, - layout: layout.clone().unwrap_widget_layout(action_input_mapping).layout, - }, - LayoutTarget::ToolShelf => FrontendMessage::UpdateToolShelfLayout { - layout_target, - layout: layout.clone().unwrap_widget_layout(action_input_mapping).layout, - }, - LayoutTarget::WorkingColors => FrontendMessage::UpdateWorkingColorsLayout { - layout_target, - layout: layout.clone().unwrap_widget_layout(action_input_mapping).layout, - }, + LayoutTarget::DialogDetails => FrontendMessage::UpdateDialogDetails { layout_target, diff }, + LayoutTarget::DocumentBar => FrontendMessage::UpdateDocumentBarLayout { layout_target, diff }, + LayoutTarget::DocumentMode => FrontendMessage::UpdateDocumentModeLayout { layout_target, diff }, + LayoutTarget::LayerTreeOptions => FrontendMessage::UpdateLayerTreeOptionsLayout { layout_target, diff }, + LayoutTarget::MenuBar => unreachable!("Menu bar is not diffed"), + LayoutTarget::NodeGraphBar => FrontendMessage::UpdateNodeGraphBarLayout { layout_target, diff }, + LayoutTarget::PropertiesOptions => FrontendMessage::UpdatePropertyPanelOptionsLayout { layout_target, diff }, + LayoutTarget::PropertiesSections => FrontendMessage::UpdatePropertyPanelSectionsLayout { layout_target, diff }, + LayoutTarget::ToolOptions => FrontendMessage::UpdateToolOptionsLayout { layout_target, diff }, + LayoutTarget::ToolShelf => FrontendMessage::UpdateToolShelfLayout { layout_target, diff }, + LayoutTarget::WorkingColors => FrontendMessage::UpdateWorkingColorsLayout { layout_target, diff }, #[remain::unsorted] LayoutTarget::LayoutTargetLength => panic!("`LayoutTargetLength` is not a valid Layout Target and is used for array indexing"), diff --git a/editor/src/messages/layout/utility_types/layout_widget.rs b/editor/src/messages/layout/utility_types/layout_widget.rs index 9ad8653a..700477d5 100644 --- a/editor/src/messages/layout/utility_types/layout_widget.rs +++ b/editor/src/messages/layout/utility_types/layout_widget.rs @@ -34,85 +34,113 @@ pub enum Layout { MenuLayout(MenuLayout), } -impl Layout { - pub fn unwrap_widget_layout(self, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Vec) -> 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| { - let shortcut_text = tooltip_shortcut.to_keys(action_input_mapping); +/// The new value of the UI, sent as part of a diff. +/// +/// An update can represent a single widget or an entire SubLayout, or just a single layout group. +#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] +pub enum DiffUpdate { + #[serde(rename = "subLayout")] + SubLayout(SubLayout), + #[serde(rename = "layoutGroup")] + LayoutGroup(LayoutGroup), + #[serde(rename = "widget")] + Widget(WidgetHolder), +} - if let ActionKeys::Keys(_keys) = tooltip_shortcut { - if !shortcut_text.is_empty() { - if !tooltip.is_empty() { - tooltip.push(' '); - } - tooltip.push('('); - tooltip.push_str(&shortcut_text); - tooltip.push(')'); +/// A single change to part of the UI, containing the location of the change and the new value. +#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] +pub struct WidgetDiff { + /// A path to the change + /// e.g. [0, 1, 2] in the properties panel is the first section, second row and third widget. + /// An empty path [] shows that the entire panel has changed and is sent when the UI is first created. + #[serde(rename = "widgetPath")] + pub widget_path: Vec, + /// What the specified part of the UI has changed to. + #[serde(rename = "newValue")] + pub new_value: DiffUpdate, +} + +impl DiffUpdate { + /// Append the shortcut to the tooltip where applicable + pub fn apply_shortcut(&mut self, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Vec) { + // 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| { + let shortcut_text = tooltip_shortcut.to_keys(action_input_mapping); + + if let ActionKeys::Keys(_keys) = tooltip_shortcut { + 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 + let convert_tooltip = |widget_holder: &mut WidgetHolder| { + // Handle all the widgets that have tooltips + let mut tooltip_shortcut = match &mut widget_holder.widget { + Widget::BreadcrumbTrailButtons(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)), + Widget::CheckboxInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)), + Widget::ColorInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)), + Widget::DropdownInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)), + Widget::FontInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)), + Widget::IconButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)), + Widget::LayerReferenceInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)), + Widget::NumberInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)), + Widget::OptionalInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)), + Widget::ParameterExposeButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)), + Widget::PopoverButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)), + Widget::TextButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)), + Widget::IconLabel(_) + | Widget::InvisibleStandinInput(_) + | Widget::PivotAssist(_) + | Widget::RadioInput(_) + | Widget::Separator(_) + | Widget::SwatchPairInput(_) + | Widget::TextAreaInput(_) + | Widget::TextInput(_) + | Widget::TextLabel(_) => None, }; + if let Some((tooltip, Some(tooltip_shortcut))) = &mut tooltip_shortcut { + apply_shortcut_to_tooltip(tooltip_shortcut, tooltip); + } - // 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::BreadcrumbTrailButtons(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)), - Widget::CheckboxInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)), - Widget::ColorInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)), - Widget::DropdownInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)), - Widget::FontInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)), - Widget::IconButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)), - Widget::LayerReferenceInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)), - Widget::NumberInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)), - Widget::OptionalInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)), - Widget::ParameterExposeButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)), - Widget::PopoverButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)), - Widget::TextButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)), - Widget::IconLabel(_) - | Widget::InvisibleStandinInput(_) - | Widget::PivotAssist(_) - | Widget::RadioInput(_) - | Widget::Separator(_) - | Widget::SwatchPairInput(_) - | Widget::TextAreaInput(_) - | Widget::TextInput(_) - | Widget::TextLabel(_) => 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); - } + // 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) + match self { + Self::SubLayout(sub_layout) => sub_layout.iter_mut().flat_map(|group| group.iter_mut()).for_each(convert_tooltip), + Self::LayoutGroup(layout_group) => layout_group.iter_mut().for_each(convert_tooltip), + Self::Widget(widget_holder) => convert_tooltip(widget_holder), } } +} +impl Layout { pub fn unwrap_menu_layout(self, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Vec) -> 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 + if let Self::MenuLayout(mut menu) = self { + menu.layout + .iter_mut() + .for_each(|menu_column| menu_column.children.fill_in_shortcut_actions_with_keys(action_input_mapping)); + menu } else { - panic!("Tried to unwrap layout as MenuLayout. Got {:?}", self) + panic!("Called unwrap_menu_layout on a widget layout"); } } @@ -129,6 +157,24 @@ impl Layout { Layout::WidgetLayout(widget_layout) => Box::new(widget_layout.iter_mut()), } } + + /// Diffing updates self (where self is old) based on new, updating the list of modifications as it does so. + pub fn diff(&mut self, new: Self, widget_path: &mut Vec, widget_diffs: &mut Vec) { + match (self, new) { + // Simply diff the internal layout + (Self::WidgetLayout(current), Self::WidgetLayout(new)) => current.diff(new, widget_path, widget_diffs), + (current, Self::WidgetLayout(widget_layout)) => { + // Upate current to the new value + *current = Self::WidgetLayout(widget_layout.clone()); + + // Push an update sublayout value + let new_value = DiffUpdate::SubLayout(widget_layout.layout); + let widget_path = widget_path.to_vec(); + widget_diffs.push(WidgetDiff { widget_path, new_value }); + } + (_, Self::MenuLayout(_)) => panic!("Cannot diff menu layout"), + } + } } impl Default for Layout { @@ -160,6 +206,30 @@ impl WidgetLayout { current_slice: None, } } + + /// Diffing updates self (where self is old) based on new, updating the list of modifications as it does so. + pub fn diff(&mut self, new: Self, widget_path: &mut Vec, widget_diffs: &mut Vec) { + // Check if the length of items is different + // TODO: Diff insersion and deletion of items + if self.layout.len() != new.layout.len() { + // Update the layout to the new layout + self.layout = new.layout.clone(); + + // Push an update sublayout to the diff + let new = DiffUpdate::SubLayout(new.layout); + widget_diffs.push(WidgetDiff { + widget_path: widget_path.to_vec(), + new_value: new, + }); + return; + } + // Diff all of the children + for (index, (current_child, new_child)) in self.layout.iter_mut().zip(new.layout.into_iter()).enumerate() { + widget_path.push(index); + current_child.diff(new_child, widget_path, widget_diffs); + widget_path.pop(); + } + } } #[derive(Debug, Default)] @@ -296,6 +366,73 @@ impl LayoutGroup { Self::Row { widgets } } } + + /// Diffing updates self (where self is old) based on new, updating the list of modifications as it does so. + pub fn diff(&mut self, new: Self, widget_path: &mut Vec, widget_diffs: &mut Vec) { + let is_column = matches!(new, Self::Column { .. }); + match (self, new) { + (Self::Column { widgets: current_widgets }, Self::Column { widgets: new_widgets }) | (Self::Row { widgets: current_widgets }, Self::Row { widgets: new_widgets }) => { + // If the lengths are different then resend the entire panel + // TODO: Diff insersion and deletion of items + if current_widgets.len() != new_widgets.len() { + // Update to the new value + *current_widgets = new_widgets.clone(); + + // Push back a LayoutGroup update to the diff + let new_value = DiffUpdate::LayoutGroup(if is_column { Self::Column { widgets: new_widgets } } else { Self::Row { widgets: new_widgets } }); + let widget_path = widget_path.to_vec(); + widget_diffs.push(WidgetDiff { widget_path, new_value }); + return; + } + // Diff all of the children + for (index, (current_child, new_child)) in current_widgets.iter_mut().zip(new_widgets.into_iter()).enumerate() { + widget_path.push(index); + current_child.diff(new_child, widget_path, widget_diffs); + widget_path.pop(); + } + } + ( + Self::Section { + name: current_name, + layout: current_layout, + }, + Self::Section { name: new_name, layout: new_layout }, + ) => { + // If the lengths are different then resend the entire panel + // TODO: Diff insersion and deletion of items + if *current_name != new_name || current_layout.len() != new_layout.len() { + // Update self to reflect new changes + *current_name = new_name.clone(); + *current_layout = new_layout.clone(); + + // Push an update layout group to the diff + let new_value = DiffUpdate::LayoutGroup(Self::Section { name: new_name, layout: new_layout }); + let widget_path = widget_path.to_vec(); + widget_diffs.push(WidgetDiff { widget_path, new_value }); + return; + } + // Diff all of the children + for (index, (current_child, new_child)) in current_layout.iter_mut().zip(new_layout.into_iter()).enumerate() { + widget_path.push(index); + current_child.diff(new_child, widget_path, widget_diffs); + widget_path.pop(); + } + } + (current, new) => { + *current = new.clone(); + let new_value = DiffUpdate::LayoutGroup(new); + let widget_path = widget_path.to_vec(); + widget_diffs.push(WidgetDiff { widget_path, new_value }); + } + } + } + + pub fn iter_mut(&mut self) -> WidgetIterMut<'_> { + WidgetIterMut { + stack: vec![self], + current_slice: None, + } + } } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -334,6 +471,22 @@ impl WidgetHolder { ..Default::default() })) } + /// Diffing updates self (where self is old) based on new, updating the list of modifications as it does so. + pub fn diff(&mut self, new: Self, widget_path: &mut [usize], widget_diffs: &mut Vec) { + // If there have been changes to the acutal widget (not just the id) + if self.widget != new.widget { + // We should update to the new widget value as well as a new widget id + *self = new.clone(); + + // Push a widget update to the diff + let new_value = DiffUpdate::Widget(new); + let widget_path = widget_path.to_vec(); + widget_diffs.push(WidgetDiff { widget_path, new_value }); + } else { + // Required to update the callback function, which the PartialEq check above skips + self.widget = new.widget; + } + } } #[derive(Clone)] diff --git a/frontend/src/components/panels/Document.vue b/frontend/src/components/panels/Document.vue index 6c7cc5ae..6f11de64 100644 --- a/frontend/src/components/panels/Document.vue +++ b/frontend/src/components/panels/Document.vue @@ -231,6 +231,7 @@ import { textInputCleanup } from "@/utility-functions/keyboard-entry"; import { rasterizeSVGCanvas } from "@/utility-functions/rasterization"; import { defaultWidgetLayout, + patchWidgetLayout, type DisplayEditableTextbox, type MouseCursorIcon, type UpdateDocumentBarLayout, @@ -505,19 +506,19 @@ export default defineComponent({ }, // Update layouts updateDocumentModeLayout(updateDocumentModeLayout: UpdateDocumentModeLayout) { - this.documentModeLayout = updateDocumentModeLayout; + patchWidgetLayout(this.documentModeLayout, updateDocumentModeLayout); }, updateToolOptionsLayout(updateToolOptionsLayout: UpdateToolOptionsLayout) { - this.toolOptionsLayout = updateToolOptionsLayout; + patchWidgetLayout(this.toolOptionsLayout, updateToolOptionsLayout); }, updateDocumentBarLayout(updateDocumentBarLayout: UpdateDocumentBarLayout) { - this.documentBarLayout = updateDocumentBarLayout; + patchWidgetLayout(this.documentBarLayout, updateDocumentBarLayout); }, updateToolShelfLayout(updateToolShelfLayout: UpdateToolShelfLayout) { - this.toolShelfLayout = updateToolShelfLayout; + patchWidgetLayout(this.toolShelfLayout, updateToolShelfLayout); }, updateWorkingColorsLayout(updateWorkingColorsLayout: UpdateWorkingColorsLayout) { - this.workingColorsLayout = updateWorkingColorsLayout; + patchWidgetLayout(this.workingColorsLayout, updateWorkingColorsLayout); }, // Resize elements to render the new viewport size viewportResize() { diff --git a/frontend/src/components/panels/LayerTree.vue b/frontend/src/components/panels/LayerTree.vue index 51176688..bb37832f 100644 --- a/frontend/src/components/panels/LayerTree.vue +++ b/frontend/src/components/panels/LayerTree.vue @@ -277,6 +277,7 @@ import { type LayerTypeData, type LayerPanelEntry, defaultWidgetLayout, + patchWidgetLayout, UpdateDocumentLayerDetails, UpdateDocumentLayerTreeStructureJs, UpdateLayerTreeOptionsLayout, @@ -523,7 +524,7 @@ export default defineComponent({ }); this.editor.subscriptions.subscribeJsMessage(UpdateLayerTreeOptionsLayout, (updateLayerTreeOptionsLayout) => { - this.layerTreeOptionsLayout = updateLayerTreeOptionsLayout; + patchWidgetLayout(this.layerTreeOptionsLayout, updateLayerTreeOptionsLayout); }); this.editor.subscriptions.subscribeJsMessage(UpdateDocumentLayerDetails, (updateDocumentLayerDetails) => { diff --git a/frontend/src/components/panels/Properties.vue b/frontend/src/components/panels/Properties.vue index 7c8cd987..09e7322b 100644 --- a/frontend/src/components/panels/Properties.vue +++ b/frontend/src/components/panels/Properties.vue @@ -32,7 +32,7 @@