Skip to content

Commit b8cb759

Browse files
committed
Add deterministic widget ids
1 parent 626fad6 commit b8cb759

File tree

4 files changed

+177
-6
lines changed

4 files changed

+177
-6
lines changed

editor/src/messages/layout/layout_message_handler.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use graphene_std::raster::color::Color;
55
use graphene_std::text::Font;
66
use graphene_std::vector::style::{FillChoice, GradientStops};
77
use serde_json::Value;
8+
use std::collections::HashMap;
89

910
#[derive(ExtractField)]
1011
pub struct LayoutMessageContext<'a> {
@@ -470,10 +471,18 @@ impl LayoutMessageHandler {
470471
fn diff_and_send_layout_to_frontend(
471472
&mut self,
472473
layout_target: LayoutTarget,
473-
new_layout: Layout,
474+
mut new_layout: Layout,
474475
responses: &mut VecDeque<Message>,
475476
action_input_mapping: &impl Fn(&MessageDiscriminant) -> Option<KeysGroup>,
476477
) {
478+
// Step 1: Collect CheckboxId mappings from new layout
479+
let mut checkbox_map = HashMap::new();
480+
new_layout.collect_checkbox_ids(layout_target, &mut Vec::new(), &mut checkbox_map);
481+
482+
// Step 2: Replace all IDs in new layout with deterministic ones
483+
new_layout.replace_widget_ids(layout_target, &mut Vec::new(), &checkbox_map);
484+
485+
// Step 3: Diff with deterministic IDs
477486
let mut widget_diffs = Vec::new();
478487

479488
self.layouts[layout_target as usize].diff(new_layout, &mut Vec::new(), &mut widget_diffs);

editor/src/messages/layout/utility_types/layout_widget.rs

Lines changed: 165 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ use super::widgets::label_widgets::*;
44
use crate::application::generate_uuid;
55
use crate::messages::input_mapper::utility_types::input_keyboard::KeysGroup;
66
use crate::messages::prelude::*;
7+
use std::collections::HashMap;
8+
use std::collections::hash_map::DefaultHasher;
9+
use std::hash::{Hash, Hasher};
710
use std::sync::Arc;
811

912
#[repr(transparent)]
@@ -111,6 +114,43 @@ pub trait Diffable: Clone + PartialEq {
111114

112115
/// Computes the diff between self (old) and new, updating self and recording changes.
113116
fn diff(&mut self, new: Self, widget_path: &mut Vec<usize>, widget_diffs: &mut Vec<WidgetDiff>);
117+
118+
/// Collects all CheckboxIds currently in use in this layout, computing stable replacements.
119+
fn collect_checkbox_ids(&self, layout_target: LayoutTarget, widget_path: &mut Vec<usize>, checkbox_map: &mut HashMap<CheckboxId, CheckboxId>);
120+
121+
/// Replaces all widget IDs with deterministic IDs based on position and type.
122+
/// Also replaces CheckboxIds using the provided mapping.
123+
fn replace_widget_ids(&mut self, layout_target: LayoutTarget, widget_path: &mut Vec<usize>, checkbox_map: &HashMap<CheckboxId, CheckboxId>);
124+
}
125+
126+
/// Computes a deterministic WidgetId based on layout target, path, and widget type.
127+
fn compute_widget_id(layout_target: LayoutTarget, widget_path: &[usize], widget: &Widget) -> WidgetId {
128+
let mut hasher = DefaultHasher::new();
129+
130+
// Hash the layout target
131+
(layout_target as u8).hash(&mut hasher);
132+
133+
// Hash the widget path
134+
widget_path.hash(&mut hasher);
135+
136+
// Hash the widget type discriminant
137+
std::mem::discriminant(widget).hash(&mut hasher);
138+
139+
WidgetId(hasher.finish())
140+
}
141+
142+
/// Computes a deterministic CheckboxId based on the same WidgetId algorithm.
143+
fn compute_checkbox_id(layout_target: LayoutTarget, widget_path: &[usize], widget: &Widget) -> CheckboxId {
144+
let mut hasher = DefaultHasher::new();
145+
146+
(layout_target as u8).hash(&mut hasher);
147+
widget_path.hash(&mut hasher);
148+
std::mem::discriminant(widget).hash(&mut hasher);
149+
150+
// Add extra salt for checkbox to differentiate from widget ID
151+
"checkbox".hash(&mut hasher);
152+
153+
CheckboxId(hasher.finish())
114154
}
115155

116156
/// Contains an arrangement of widgets mounted somewhere specific in the frontend.
@@ -159,6 +199,22 @@ impl Diffable for Layout {
159199
widget_path.pop();
160200
}
161201
}
202+
203+
fn collect_checkbox_ids(&self, layout_target: LayoutTarget, widget_path: &mut Vec<usize>, checkbox_map: &mut HashMap<CheckboxId, CheckboxId>) {
204+
for (index, child) in self.0.iter().enumerate() {
205+
widget_path.push(index);
206+
child.collect_checkbox_ids(layout_target, widget_path, checkbox_map);
207+
widget_path.pop();
208+
}
209+
}
210+
211+
fn replace_widget_ids(&mut self, layout_target: LayoutTarget, widget_path: &mut Vec<usize>, checkbox_map: &HashMap<CheckboxId, CheckboxId>) {
212+
for (index, child) in self.0.iter_mut().enumerate() {
213+
widget_path.push(index);
214+
child.replace_widget_ids(layout_target, widget_path, checkbox_map);
215+
widget_path.pop();
216+
}
217+
}
162218
}
163219

164220
#[derive(Debug, Default)]
@@ -474,6 +530,58 @@ impl Diffable for LayoutGroup {
474530
}
475531
}
476532
}
533+
534+
fn collect_checkbox_ids(&self, layout_target: LayoutTarget, widget_path: &mut Vec<usize>, checkbox_map: &mut HashMap<CheckboxId, CheckboxId>) {
535+
match self {
536+
Self::Column { widgets } | Self::Row { widgets } => {
537+
for (index, widget) in widgets.iter().enumerate() {
538+
widget_path.push(index);
539+
widget.collect_checkbox_ids(layout_target, widget_path, checkbox_map);
540+
widget_path.pop();
541+
}
542+
}
543+
Self::Table { rows, .. } => {
544+
for (row_idx, row) in rows.iter().enumerate() {
545+
for (col_idx, widget) in row.iter().enumerate() {
546+
widget_path.push(row_idx);
547+
widget_path.push(col_idx);
548+
widget.collect_checkbox_ids(layout_target, widget_path, checkbox_map);
549+
widget_path.pop();
550+
widget_path.pop();
551+
}
552+
}
553+
}
554+
Self::Section { layout, .. } => {
555+
layout.collect_checkbox_ids(layout_target, widget_path, checkbox_map);
556+
}
557+
}
558+
}
559+
560+
fn replace_widget_ids(&mut self, layout_target: LayoutTarget, widget_path: &mut Vec<usize>, checkbox_map: &HashMap<CheckboxId, CheckboxId>) {
561+
match self {
562+
Self::Column { widgets } | Self::Row { widgets } => {
563+
for (index, widget) in widgets.iter_mut().enumerate() {
564+
widget_path.push(index);
565+
widget.replace_widget_ids(layout_target, widget_path, checkbox_map);
566+
widget_path.pop();
567+
}
568+
}
569+
Self::Table { rows, .. } => {
570+
for (row_idx, row) in rows.iter_mut().enumerate() {
571+
for (col_idx, widget) in row.iter_mut().enumerate() {
572+
widget_path.push(row_idx);
573+
widget_path.push(col_idx);
574+
widget.replace_widget_ids(layout_target, widget_path, checkbox_map);
575+
widget_path.pop();
576+
widget_path.pop();
577+
}
578+
}
579+
}
580+
Self::Section { layout, .. } => {
581+
layout.replace_widget_ids(layout_target, widget_path, checkbox_map);
582+
}
583+
}
584+
}
477585
}
478586

479587
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, specta::Type)]
@@ -485,7 +593,7 @@ pub struct WidgetInstance {
485593

486594
impl PartialEq for WidgetInstance {
487595
fn eq(&self, other: &Self) -> bool {
488-
self.widget == other.widget
596+
self.widget_id == other.widget_id && self.widget == other.widget
489597
}
490598
}
491599

@@ -537,6 +645,62 @@ impl Diffable for WidgetInstance {
537645
self.widget = new.widget;
538646
}
539647
}
648+
649+
fn collect_checkbox_ids(&self, layout_target: LayoutTarget, widget_path: &mut Vec<usize>, checkbox_map: &mut HashMap<CheckboxId, CheckboxId>) {
650+
match &self.widget {
651+
Widget::CheckboxInput(checkbox) => {
652+
// Compute stable ID based on position and insert mapping
653+
let checkbox_id = checkbox.for_label;
654+
let stable_id = compute_checkbox_id(layout_target, widget_path, &self.widget);
655+
checkbox_map.entry(checkbox_id).or_insert(stable_id);
656+
}
657+
Widget::TextLabel(label) => {
658+
// Compute stable ID based on position and insert mapping
659+
let checkbox_id = label.for_checkbox;
660+
let stable_id = compute_checkbox_id(layout_target, widget_path, &self.widget);
661+
checkbox_map.entry(checkbox_id).or_insert(stable_id);
662+
}
663+
Widget::PopoverButton(button) => {
664+
// Recursively collect from nested popover layout
665+
for (index, child) in button.popover_layout.0.iter().enumerate() {
666+
widget_path.push(index);
667+
child.collect_checkbox_ids(layout_target, widget_path, checkbox_map);
668+
widget_path.pop();
669+
}
670+
}
671+
_ => {}
672+
}
673+
}
674+
675+
fn replace_widget_ids(&mut self, layout_target: LayoutTarget, widget_path: &mut Vec<usize>, checkbox_map: &HashMap<CheckboxId, CheckboxId>) {
676+
// 1. Generate deterministic WidgetId
677+
self.widget_id = compute_widget_id(layout_target, widget_path, &self.widget);
678+
679+
// 2. Replace CheckboxIds if present
680+
match &mut self.widget {
681+
Widget::CheckboxInput(checkbox) => {
682+
let old_id = checkbox.for_label;
683+
if let Some(&new_id) = checkbox_map.get(&old_id) {
684+
checkbox.for_label = new_id;
685+
}
686+
}
687+
Widget::TextLabel(label) => {
688+
let old_id = label.for_checkbox;
689+
if let Some(&new_id) = checkbox_map.get(&old_id) {
690+
label.for_checkbox = new_id;
691+
}
692+
}
693+
Widget::PopoverButton(button) => {
694+
// Recursively replace in nested popover layout
695+
for (index, child) in button.popover_layout.0.iter_mut().enumerate() {
696+
widget_path.push(index);
697+
child.replace_widget_ids(layout_target, widget_path, checkbox_map);
698+
widget_path.pop();
699+
}
700+
}
701+
_ => {}
702+
}
703+
}
540704
}
541705

542706
#[derive(Clone, specta::Type)]

editor/src/messages/layout/utility_types/widgets/input_widgets.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ pub struct CheckboxInput {
2626
pub tooltip_shortcut: Option<ActionShortcut>,
2727

2828
#[serde(rename = "forLabel")]
29-
#[derivative(Debug = "ignore", PartialEq = "ignore")]
3029
pub for_label: CheckboxId,
3130

3231
// Callbacks
@@ -55,8 +54,8 @@ impl Default for CheckboxInput {
5554
}
5655
}
5756

58-
#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
59-
pub struct CheckboxId(u64);
57+
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)]
58+
pub struct CheckboxId(pub u64);
6059

6160
impl CheckboxId {
6261
pub fn new() -> Self {

editor/src/messages/layout/utility_types/widgets/label_widgets.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,6 @@ pub struct TextLabel {
7878
pub tooltip_shortcut: Option<ActionShortcut>,
7979

8080
#[serde(rename = "forCheckbox")]
81-
#[derivative(PartialEq = "ignore")]
8281
pub for_checkbox: CheckboxId,
8382

8483
// Body

0 commit comments

Comments
 (0)