Diff simple layout changes to avoid excessive DOM replacements (#910)

* Add UI diffs to rust

* Clean up some js

* Fix lints

* Fix test

* Remove one unnecessary keyword

* Rename to widget path

* Rename new_val to new_value

* Rename newVal to layoutGroup in createLayoutGroup

* Extract get_widget_path to a function

* Base skipping on the layout rather than the target

* Rename to ResendActiveWidget

* Switch info to trace

* Add a link to the documentation about Object.assign

* knitpick js changes

* Add more comments to diff functions

Co-authored-by: mfish33 <maxmfishernj@gmail.com>
This commit is contained in:
0HyperCube 2022-12-25 18:56:35 +00:00 committed by Keavon Chambers
parent 8d7e6c530e
commit d742b05d3a
12 changed files with 464 additions and 264 deletions

View File

@ -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);
}
}
}
}

View File

@ -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<WidgetDiff>,
},
UpdateDocumentArtboards {
svg: String,
@ -154,7 +154,7 @@ pub enum FrontendMessage {
UpdateDocumentBarLayout {
#[serde(rename = "layoutTarget")]
layout_target: LayoutTarget,
layout: SubLayout,
diff: Vec<WidgetDiff>,
},
UpdateDocumentLayerDetails {
data: LayerPanelEntry,
@ -170,7 +170,7 @@ pub enum FrontendMessage {
UpdateDocumentModeLayout {
#[serde(rename = "layoutTarget")]
layout_target: LayoutTarget,
layout: SubLayout,
diff: Vec<WidgetDiff>,
},
UpdateDocumentOverlays {
svg: String,
@ -208,7 +208,7 @@ pub enum FrontendMessage {
UpdateLayerTreeOptionsLayout {
#[serde(rename = "layoutTarget")]
layout_target: LayoutTarget,
layout: SubLayout,
diff: Vec<WidgetDiff>,
},
UpdateMenuBarLayout {
#[serde(rename = "layoutTarget")]
@ -225,7 +225,7 @@ pub enum FrontendMessage {
UpdateNodeGraphBarLayout {
#[serde(rename = "layoutTarget")]
layout_target: LayoutTarget,
layout: SubLayout,
diff: Vec<WidgetDiff>,
},
UpdateNodeGraphSelection {
selected: Vec<NodeId>,
@ -244,26 +244,26 @@ pub enum FrontendMessage {
UpdatePropertyPanelOptionsLayout {
#[serde(rename = "layoutTarget")]
layout_target: LayoutTarget,
layout: SubLayout,
diff: Vec<WidgetDiff>,
},
UpdatePropertyPanelSectionsLayout {
#[serde(rename = "layoutTarget")]
layout_target: LayoutTarget,
layout: SubLayout,
diff: Vec<WidgetDiff>,
},
UpdateToolOptionsLayout {
#[serde(rename = "layoutTarget")]
layout_target: LayoutTarget,
layout: SubLayout,
diff: Vec<WidgetDiff>,
},
UpdateToolShelfLayout {
#[serde(rename = "layoutTarget")]
layout_target: LayoutTarget,
layout: SubLayout,
diff: Vec<WidgetDiff>,
},
UpdateWorkingColorsLayout {
#[serde(rename = "layoutTarget")]
layout_target: LayoutTarget,
layout: SubLayout,
diff: Vec<WidgetDiff>,
},
}

View File

@ -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 },
}

View File

@ -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<usize>)> {
let mut stack = widget_layout.layout.iter().enumerate().map(|(index, val)| (vec![index], val)).collect::<Vec<_>>();
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<F: Fn(&MessageDiscriminant) -> Vec<KeysGroup>> MessageHandler<LayoutMessage, F> for LayoutMessageHandler {
#[remain::check]
fn process_message(&mut self, message: LayoutMessage, data: F, responses: &mut std::collections::VecDeque<Message>) {
@ -24,14 +51,22 @@ impl<F: Fn(&MessageDiscriminant) -> Vec<KeysGroup>> MessageHandler<LayoutMessage
use LayoutMessage::*;
#[remain::sorted]
match message {
RefreshLayout { layout_target } => {
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<F: Fn(&MessageDiscriminant) -> Vec<KeysGroup>> MessageHandler<LayoutMessage
Some(None)
}
})()
.unwrap_or_else(|| panic!("ColorInput update was not able to be parsed with color data: {:?}", color_input));
.unwrap_or_else(|| panic!("ColorInput update was not able to be parsed with color data: {color_input:?}"));
color_input.value = parsed_color;
let callback_message = (color_input.on_update.callback)(color_input);
responses.push_back(callback_message);
@ -197,7 +232,7 @@ impl<F: Fn(&MessageDiscriminant) -> Vec<KeysGroup>> MessageHandler<LayoutMessage
}
Widget::TextLabel(_) => {}
};
responses.push_back(RefreshLayout { layout_target }.into());
responses.push_back(ResendActiveWidget { layout_target, dirty_id: widget_id }.into());
}
}
}
@ -208,55 +243,57 @@ impl<F: Fn(&MessageDiscriminant) -> Vec<KeysGroup>> MessageHandler<LayoutMessage
}
impl LayoutMessageHandler {
/// Diff the update and send to the frontend where necessary
fn send_layout(&mut self, layout_target: LayoutTarget, new_layout: Layout, responses: &mut VecDeque<Message>, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Vec<KeysGroup>) {
// 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<Message>, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Vec<KeysGroup>) {
let layout = &self.layouts[layout_target as usize];
fn send_diff(&self, mut diff: Vec<WidgetDiff>, layout_target: LayoutTarget, responses: &mut VecDeque<Message>, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Vec<KeysGroup>) {
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"),

View File

@ -34,85 +34,113 @@ pub enum Layout {
MenuLayout(MenuLayout),
}
impl Layout {
pub fn unwrap_widget_layout(self, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Vec<KeysGroup>) -> 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<usize>,
/// 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<KeysGroup>) {
// 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<KeysGroup>) -> 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<usize>, widget_diffs: &mut Vec<WidgetDiff>) {
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<usize>, widget_diffs: &mut Vec<WidgetDiff>) {
// 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<usize>, widget_diffs: &mut Vec<WidgetDiff>) {
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<WidgetDiff>) {
// 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)]

View File

@ -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() {

View File

@ -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) => {

View File

@ -32,7 +32,7 @@
<script lang="ts">
import { defineComponent } from "vue";
import { defaultWidgetLayout, UpdatePropertyPanelOptionsLayout, UpdatePropertyPanelSectionsLayout } from "@/wasm-communication/messages";
import { defaultWidgetLayout, patchWidgetLayout, UpdatePropertyPanelOptionsLayout, UpdatePropertyPanelSectionsLayout } from "@/wasm-communication/messages";
import LayoutCol from "@/components/layout/LayoutCol.vue";
import LayoutRow from "@/components/layout/LayoutRow.vue";
@ -48,11 +48,11 @@ export default defineComponent({
},
mounted() {
this.editor.subscriptions.subscribeJsMessage(UpdatePropertyPanelOptionsLayout, (updatePropertyPanelOptionsLayout) => {
this.propertiesOptionsLayout = updatePropertyPanelOptionsLayout;
patchWidgetLayout(this.propertiesOptionsLayout, updatePropertyPanelOptionsLayout);
});
this.editor.subscriptions.subscribeJsMessage(UpdatePropertyPanelSectionsLayout, (updatePropertyPanelSectionsLayout) => {
this.propertiesSectionsLayout = updatePropertyPanelSectionsLayout;
patchWidgetLayout(this.propertiesSectionsLayout, updatePropertyPanelSectionsLayout);
});
},
components: {

View File

@ -2,7 +2,7 @@ import { reactive, readonly } from "vue";
import { type IconName } from "@/utility-functions/icons";
import { type Editor } from "@/wasm-communication/editor";
import { type TextButtonWidget, type WidgetLayout, defaultWidgetLayout, DisplayDialog, DisplayDialogDismiss, UpdateDialogDetails } from "@/wasm-communication/messages";
import { type TextButtonWidget, type WidgetLayout, defaultWidgetLayout, DisplayDialog, DisplayDialogDismiss, UpdateDialogDetails, patchWidgetLayout } from "@/wasm-communication/messages";
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function createDialogState(editor: Editor) {
@ -37,7 +37,7 @@ export function createDialogState(editor: Editor) {
state.icon = displayDialog.icon;
});
editor.subscriptions.subscribeJsMessage(UpdateDialogDetails, (updateDialogDetails) => {
state.widgets = updateDialogDetails;
patchWidgetLayout(state.widgets, updateDialogDetails);
state.jsCallbackBasedButtons = undefined;
});
editor.subscriptions.subscribeJsMessage(DisplayDialogDismiss, dismissDialog);

View File

@ -1,7 +1,16 @@
import { reactive, readonly } from "vue";
import { type Editor } from "@/wasm-communication/editor";
import { type FrontendNode, type FrontendNodeLink, type FrontendNodeType, UpdateNodeGraph, UpdateNodeTypes, UpdateNodeGraphBarLayout, defaultWidgetLayout } from "@/wasm-communication/messages";
import {
type FrontendNode,
type FrontendNodeLink,
type FrontendNodeType,
UpdateNodeGraph,
UpdateNodeTypes,
UpdateNodeGraphBarLayout,
defaultWidgetLayout,
patchWidgetLayout,
} from "@/wasm-communication/messages";
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function createNodeGraphState(editor: Editor) {
@ -21,7 +30,7 @@ export function createNodeGraphState(editor: Editor) {
state.nodeTypes = updateNodeTypes.nodeTypes;
});
editor.subscriptions.subscribeJsMessage(UpdateNodeGraphBarLayout, (updateNodeGraphBarLayout) => {
state.nodeGraphBarLayout = updateNodeGraphBarLayout;
patchWidgetLayout(state.nodeGraphBarLayout, updateNodeGraphBarLayout);
});
return {

View File

@ -1171,18 +1171,20 @@ export class Widget {
widgetId!: bigint;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function hoistWidgetHolder(widgetHolder: any): Widget {
const kind = Object.keys(widgetHolder.widget)[0];
const props = widgetHolder.widget[kind];
props.kind = kind;
const { widgetId } = widgetHolder;
return plainToClass(Widget, { props, widgetId });
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function hoistWidgetHolders(widgetHolders: any[]): Widget[] {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return widgetHolders.map((widgetHolder: any) => {
const kind = Object.keys(widgetHolder.widget)[0];
const props = widgetHolder.widget[kind];
props.kind = kind;
const { widgetId } = widgetHolder;
return plainToClass(Widget, { props, widgetId });
});
return widgetHolders.map(hoistWidgetHolder);
}
// WIDGET LAYOUT
@ -1192,6 +1194,18 @@ export type WidgetLayout = {
layout: LayoutGroup[];
};
export class WidgetDiffUpdate extends JsMessage {
layoutTarget!: unknown;
// TODO: Replace `any` with correct typing
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@Transform(({ value }: { value: any }) => createWidgetDiff(value))
diff!: WidgetDiff[];
}
type UIItem = LayoutGroup[] | LayoutGroup | Widget | MenuBarEntry[] | MenuBarEntry;
type WidgetDiff = { widgetPath: number[]; newValue: UIItem };
export function defaultWidgetLayout(): WidgetLayout {
return {
layoutTarget: undefined,
@ -1199,6 +1213,42 @@ export function defaultWidgetLayout(): WidgetLayout {
};
}
// Updates a widget layout based on a list of updates, returning the new layout
export function patchWidgetLayout(layout: WidgetLayout, updates: WidgetDiffUpdate): void {
layout.layoutTarget = updates.layoutTarget;
updates.diff.forEach((update) => {
// Find the object where the diff applies to
const diffObject = update.widgetPath.reduce((targetLayout, index) => {
if ("columnWidgets" in targetLayout) return targetLayout.columnWidgets[index];
if ("rowWidgets" in targetLayout) return targetLayout.rowWidgets[index];
if ("layout" in targetLayout) return targetLayout.layout[index];
if (targetLayout instanceof Widget) {
// eslint-disable-next-line no-console
console.error("Tried to index widget");
return targetLayout;
}
// This is a path traversal so we can assume from the backend that it exists
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
if ("action" in targetLayout) return targetLayout.children![index];
return targetLayout[index];
}, layout.layout as UIItem);
// If this is a list with a length, then set the length to 0 to clear the list
if ("length" in diffObject) {
diffObject.length = 0;
}
// Remove all of the keys from the old object
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Object.keys(diffObject).forEach((key) => delete (diffObject as any)[key]);
// Assign keys to the new object
// `Object.assign` works but `diffObject = update.newValue;` doesn't.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
Object.assign(diffObject, update.newValue);
});
}
export type LayoutGroup = WidgetRow | WidgetColumn | WidgetSection;
export type WidgetColumn = { columnWidgets: Widget[] };
@ -1218,116 +1268,66 @@ export function isWidgetSection(layoutRow: LayoutGroup): layoutRow is WidgetSect
// Unpacking rust types to more usable type in the frontend
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function createWidgetLayout(widgetLayout: any[]): LayoutGroup[] {
return widgetLayout.map((layoutType): LayoutGroup => {
if (layoutType.column) {
const columnWidgets = hoistWidgetHolders(layoutType.column.columnWidgets);
const result: WidgetColumn = { columnWidgets };
return result;
function createWidgetDiff(diffs: any[]): WidgetDiff[] {
return diffs.map((diff) => {
const { widgetPath, newValue } = diff;
if (newValue.subLayout) {
return { widgetPath, newValue: newValue.subLayout.map(createLayoutGroup) };
}
if (layoutType.row) {
const rowWidgets = hoistWidgetHolders(layoutType.row.rowWidgets);
const result: WidgetRow = { rowWidgets };
return result;
if (newValue.layoutGroup) {
return { widgetPath, newValue: createLayoutGroup(newValue.layoutGroup) };
}
if (layoutType.section) {
const { name } = layoutType.section;
const layout = createWidgetLayout(layoutType.section.layout);
const result: WidgetSection = { name, layout };
return result;
if (newValue.widget) {
return { widgetPath, newValue: hoistWidgetHolder(newValue.widget) };
}
throw new Error("Layout row type does not exist");
// This code should be unreachable
throw new Error("DiffUpdate invalid");
});
}
// Unpacking a layout group
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function createLayoutGroup(layoutGroup: any): LayoutGroup {
if (layoutGroup.column) {
const columnWidgets = hoistWidgetHolders(layoutGroup.column.columnWidgets);
const result: WidgetColumn = { columnWidgets };
return result;
}
if (layoutGroup.row) {
const result: WidgetRow = { rowWidgets: hoistWidgetHolders(layoutGroup.row.rowWidgets) };
return result;
}
if (layoutGroup.section) {
const result: WidgetSection = { name: layoutGroup.section.name, layout: layoutGroup.section.layout.map(createLayoutGroup) };
return result;
}
throw new Error("Layout row type does not exist");
}
// WIDGET LAYOUTS
export class UpdateDialogDetails extends WidgetDiffUpdate {}
export class UpdateDialogDetails extends JsMessage implements WidgetLayout {
layoutTarget!: unknown;
export class UpdateDocumentModeLayout extends WidgetDiffUpdate {}
// TODO: Replace `any` with correct typing
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@Transform(({ value }: { value: any }) => createWidgetLayout(value))
layout!: LayoutGroup[];
}
export class UpdateToolOptionsLayout extends WidgetDiffUpdate {}
export class UpdateDocumentModeLayout extends JsMessage implements WidgetLayout {
layoutTarget!: unknown;
export class UpdateDocumentBarLayout extends WidgetDiffUpdate {}
// TODO: Replace `any` with correct typing
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@Transform(({ value }: { value: any }) => createWidgetLayout(value))
layout!: LayoutGroup[];
}
export class UpdateToolShelfLayout extends WidgetDiffUpdate {}
export class UpdateToolOptionsLayout extends JsMessage implements WidgetLayout {
layoutTarget!: unknown;
export class UpdateWorkingColorsLayout extends WidgetDiffUpdate {}
// TODO: Replace `any` with correct typing
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@Transform(({ value }: { value: any }) => createWidgetLayout(value))
layout!: LayoutGroup[];
}
export class UpdatePropertyPanelOptionsLayout extends WidgetDiffUpdate {}
export class UpdateDocumentBarLayout extends JsMessage implements WidgetLayout {
layoutTarget!: unknown;
export class UpdatePropertyPanelSectionsLayout extends WidgetDiffUpdate {}
// TODO: Replace `any` with correct typing
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@Transform(({ value }: { value: any }) => createWidgetLayout(value))
layout!: LayoutGroup[];
}
export class UpdateLayerTreeOptionsLayout extends WidgetDiffUpdate {}
export class UpdateToolShelfLayout extends JsMessage implements WidgetLayout {
layoutTarget!: unknown;
// TODO: Replace `any` with correct typing
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@Transform(({ value }: { value: any }) => createWidgetLayout(value))
layout!: LayoutGroup[];
}
export class UpdateWorkingColorsLayout extends JsMessage implements WidgetLayout {
layoutTarget!: unknown;
// TODO: Replace `any` with correct typing
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@Transform(({ value }: { value: any }) => createWidgetLayout(value))
layout!: LayoutGroup[];
}
export class UpdatePropertyPanelOptionsLayout extends JsMessage implements WidgetLayout {
layoutTarget!: unknown;
// TODO: Replace `any` with correct typing
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@Transform(({ value }: { value: any }) => createWidgetLayout(value))
layout!: LayoutGroup[];
}
export class UpdatePropertyPanelSectionsLayout extends JsMessage implements WidgetLayout {
layoutTarget!: unknown;
// TODO: Replace `any` with correct typing
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@Transform(({ value }: { value: any }) => createWidgetLayout(value))
layout!: LayoutGroup[];
}
export class UpdateLayerTreeOptionsLayout extends JsMessage implements WidgetLayout {
layoutTarget!: unknown;
// TODO: Replace `any` with correct typing
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@Transform(({ value }: { value: any }) => createWidgetLayout(value))
layout!: LayoutGroup[];
}
export class UpdateNodeGraphBarLayout extends WidgetDiffUpdate {}
export class UpdateMenuBarLayout extends JsMessage {
layoutTarget!: unknown;
@ -1338,15 +1338,6 @@ export class UpdateMenuBarLayout extends JsMessage {
layout!: MenuBarEntry[];
}
export class UpdateNodeGraphBarLayout extends JsMessage {
layoutTarget!: unknown;
// TODO: Replace `any` with correct typing
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@Transform(({ value }: { value: any }) => createWidgetLayout(value))
layout!: LayoutGroup[];
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function createMenuLayout(menuBarEntry: any[]): MenuBarEntry[] {
return menuBarEntry.map((entry) => ({

View File

@ -0,0 +1,5 @@
[target.wasm32-unknown-unknown]
rustflags = ["-C", "target-feature=+simd128,+atomics,+bulk-memory,+mutable-globals"]
[unstable]
build-std = ["panic_abort", "std"]