Refactor the TypeScript data flow for full type safety and auto-generation of Rust types (#3865)

* Migrate Specta to Tsify to auto-generate messages.ts, working except colors and widgets

* Adopt the generated FillColor/Color/GradientStops

* Fix widget typing

* Separate WidgetGroup enum variants into wrapper structs

* Small rename

* Simplify widgets further

* Clean up message type references

* Switch type imports to the auto-generated file

* Remove lowercase serde rename

* Fix FillChoice deserialization

* Fix small regression from #3837

* Improve type safety

* Make WidgetSpan type-safe

* More cleanup and type safety

* More type safety

* More type safety

* Get the rest to type-check without errors; improve widget builder macro to have optional icons; improve Svelte 5 configs

* Cargo fmt

* Fix imports

* Update outdated readme info

* Fix lint command rename references

* Fix typos

* One more typos fix

* Remove unnecessary dep: prefix from the edited Cargo.toml files

* Remove excess parts from Cargo.toml

* Fix compiling on desktop

* Revert "Remove excess parts from Cargo.toml"

This reverts commit 6b711117b3a5d5d8a3ee20f36a43bc74930b7c82.

* Update dev docs with simpler, more accurate instructions
This commit is contained in:
Keavon Chambers
2026-03-09 16:35:04 -07:00
parent fbd2658148
commit 52d2b38a82
199 changed files with 2265 additions and 2811 deletions
@@ -15,7 +15,7 @@ impl DialogLayoutHolder for AboutGraphiteDialog {
fn layout_buttons(&self) -> Layout {
let widgets = vec![TextButton::new("OK").emphasized(true).on_update(|_| FrontendMessage::DialogClose.into()).widget_instance()];
Layout(vec![LayoutGroup::Row { widgets }])
Layout(vec![LayoutGroup::row(widgets)])
}
fn layout_column_2(&self) -> Layout {
@@ -29,7 +29,7 @@ impl DialogLayoutHolder for AboutGraphiteDialog {
.into_iter()
.map(|(icon, label, url)| {
TextButton::new(label)
.icon(Some(icon.into()))
.icon(icon)
.flush(true)
.on_update(|_| FrontendMessage::TriggerVisitLink { url: url.into() }.into())
.widget_instance()
@@ -40,7 +40,7 @@ impl DialogLayoutHolder for AboutGraphiteDialog {
let localized_commit_year = self.localized_commit_year.clone();
widgets.push(
TextButton::new("Licenses")
.icon(Some("License".into()))
.icon("License")
.flush(true)
.on_update(move |_| {
DialogMessage::RequestLicensesDialogWithLocalizedCommitDate {
@@ -51,22 +51,16 @@ impl DialogLayoutHolder for AboutGraphiteDialog {
.widget_instance(),
);
Layout(vec![LayoutGroup::Column { widgets }])
Layout(vec![LayoutGroup::column(widgets)])
}
}
impl LayoutHolder for AboutGraphiteDialog {
fn layout(&self) -> Layout {
Layout(vec![
LayoutGroup::Row {
widgets: vec![TextLabel::new("About this release").bold(true).widget_instance()],
},
LayoutGroup::Row {
widgets: vec![TextLabel::new(commit_info_localized(&self.localized_commit_date)).multiline(true).widget_instance()],
},
LayoutGroup::Row {
widgets: vec![TextLabel::new(format!("Copyright © {} Graphite contributors", self.localized_commit_year)).widget_instance()],
},
LayoutGroup::row(vec![TextLabel::new("About this release").bold(true).widget_instance()]),
LayoutGroup::row(vec![TextLabel::new(commit_info_localized(&self.localized_commit_date)).multiline(true).widget_instance()]),
LayoutGroup::row(vec![TextLabel::new(format!("Copyright © {} Graphite contributors", self.localized_commit_year)).widget_instance()]),
])
}
}
@@ -24,7 +24,7 @@ impl DialogLayoutHolder for CloseAllDocumentsDialog {
TextButton::new("Cancel").on_update(|_| FrontendMessage::DialogClose.into()).widget_instance(),
];
Layout(vec![LayoutGroup::Row { widgets }])
Layout(vec![LayoutGroup::row(widgets)])
}
}
@@ -33,12 +33,8 @@ impl LayoutHolder for CloseAllDocumentsDialog {
let unsaved_list = "• ".to_string() + &self.unsaved_document_names.join("\n• ");
Layout(vec![
LayoutGroup::Row {
widgets: vec![TextLabel::new("Save documents before closing them?").bold(true).multiline(true).widget_instance()],
},
LayoutGroup::Row {
widgets: vec![TextLabel::new(format!("Documents with unsaved changes:\n{unsaved_list}")).multiline(true).widget_instance()],
},
LayoutGroup::row(vec![TextLabel::new("Save documents before closing them?").bold(true).multiline(true).widget_instance()]),
LayoutGroup::row(vec![TextLabel::new(format!("Documents with unsaved changes:\n{unsaved_list}")).multiline(true).widget_instance()]),
])
}
}
@@ -35,7 +35,7 @@ impl DialogLayoutHolder for CloseDocumentDialog {
TextButton::new("Cancel").on_update(|_| FrontendMessage::DialogClose.into()).widget_instance(),
];
Layout(vec![LayoutGroup::Row { widgets }])
Layout(vec![LayoutGroup::row(widgets)])
}
}
@@ -52,12 +52,8 @@ impl LayoutHolder for CloseDocumentDialog {
let break_lines = if self.document_name.len() > max_one_line_length { '\n' } else { ' ' };
Layout(vec![
LayoutGroup::Row {
widgets: vec![TextLabel::new("Save document before closing it?").bold(true).widget_instance()],
},
LayoutGroup::Row {
widgets: vec![TextLabel::new(format!("\"{name}{ellipsis}\"{break_lines}has unsaved changes")).multiline(true).widget_instance()],
},
LayoutGroup::row(vec![TextLabel::new("Save document before closing it?").bold(true).widget_instance()]),
LayoutGroup::row(vec![TextLabel::new(format!("\"{name}{ellipsis}\"{break_lines}has unsaved changes")).multiline(true).widget_instance()]),
])
}
}
@@ -24,7 +24,7 @@ impl DialogLayoutHolder for ConfirmRestartDialog {
TextButton::new("Later").on_update(|_| FrontendMessage::DialogClose.into()).widget_instance(),
];
Layout(vec![LayoutGroup::Row { widgets }])
Layout(vec![LayoutGroup::row(widgets)])
}
}
@@ -33,27 +33,23 @@ impl LayoutHolder for ConfirmRestartDialog {
let changed_settings = "• ".to_string() + &self.changed_settings.join("\n• ");
Layout(vec![
LayoutGroup::Row {
widgets: vec![TextLabel::new("Restart to apply changes?").bold(true).multiline(true).widget_instance()],
},
LayoutGroup::Row {
widgets: vec![
TextLabel::new(
format!(
"
LayoutGroup::row(vec![TextLabel::new("Restart to apply changes?").bold(true).multiline(true).widget_instance()]),
LayoutGroup::row(vec![
TextLabel::new(
format!(
"
Settings that only take effect on next launch:\n\
{changed_settings}\n\
\n\
This only takes a few seconds. Open documents,\n\
even unsaved ones, will be automatically restored.
"
)
.trim(),
)
.multiline(true)
.widget_instance(),
],
},
.trim(),
)
.multiline(true)
.widget_instance(),
]),
])
}
}
@@ -22,7 +22,7 @@ impl DialogLayoutHolder for DemoArtworkDialog {
fn layout_buttons(&self) -> Layout {
let widgets = vec![TextButton::new("Close").emphasized(true).on_update(|_| FrontendMessage::DialogClose.into()).widget_instance()];
Layout(vec![LayoutGroup::Row { widgets }])
Layout(vec![LayoutGroup::row(widgets)])
}
}
@@ -46,7 +46,7 @@ impl LayoutHolder for DemoArtworkDialog {
let images = chunk
.iter()
.map(|(name, thumbnail, filename)| ImageButton::new(*thumbnail).width(Some("256px".into())).on_update(|_| make_dialog(name, filename)).widget_instance())
.map(|(name, thumbnail, filename)| ImageButton::new(*thumbnail).width("256px").on_update(|_| make_dialog(name, filename)).widget_instance())
.collect();
let buttons = chunk
@@ -54,7 +54,7 @@ impl LayoutHolder for DemoArtworkDialog {
.map(|(name, _, filename)| TextButton::new(*name).min_width(256).flush(true).on_update(|_| make_dialog(name, filename)).widget_instance())
.collect();
vec![LayoutGroup::Row { widgets: images }, LayoutGroup::Row { widgets: buttons }, LayoutGroup::Row { widgets: vec![] }]
vec![LayoutGroup::row(images), LayoutGroup::row(buttons), LayoutGroup::row(vec![])]
})
.collect();
let _ = rows_of_images_with_buttons.pop();
@@ -14,19 +14,15 @@ impl DialogLayoutHolder for ErrorDialog {
fn layout_buttons(&self) -> Layout {
let widgets = vec![TextButton::new("OK").emphasized(true).on_update(|_| FrontendMessage::DialogClose.into()).widget_instance()];
Layout(vec![LayoutGroup::Row { widgets }])
Layout(vec![LayoutGroup::row(widgets)])
}
}
impl LayoutHolder for ErrorDialog {
fn layout(&self) -> Layout {
Layout(vec![
LayoutGroup::Row {
widgets: vec![TextLabel::new(&self.title).bold(true).widget_instance()],
},
LayoutGroup::Row {
widgets: vec![TextLabel::new(&self.description).multiline(true).widget_instance()],
},
LayoutGroup::row(vec![TextLabel::new(&self.title).bold(true).widget_instance()]),
LayoutGroup::row(vec![TextLabel::new(&self.description).multiline(true).widget_instance()]),
])
}
}
@@ -12,7 +12,7 @@ impl DialogLayoutHolder for LicensesDialog {
fn layout_buttons(&self) -> Layout {
let widgets = vec![TextButton::new("OK").emphasized(true).on_update(|_| FrontendMessage::DialogClose.into()).widget_instance()];
Layout(vec![LayoutGroup::Row { widgets }])
Layout(vec![LayoutGroup::row(widgets)])
}
fn layout_column_2(&self) -> Layout {
@@ -34,10 +34,10 @@ impl DialogLayoutHolder for LicensesDialog {
];
let widgets = button_definitions
.iter()
.map(|&(icon, label, message_factory)| TextButton::new(label).icon(Some((icon).into())).flush(true).on_update(move |_| message_factory()).widget_instance())
.map(|&(icon, label, message_factory)| TextButton::new(label).icon(icon).flush(true).on_update(move |_| message_factory()).widget_instance())
.collect();
Layout(vec![LayoutGroup::Column { widgets }])
Layout(vec![LayoutGroup::column(widgets)])
}
}
@@ -56,12 +56,8 @@ impl LayoutHolder for LicensesDialog {
let description = description.trim();
Layout(vec![
LayoutGroup::Row {
widgets: vec![TextLabel::new("Graphite is free, open source software").bold(true).widget_instance()],
},
LayoutGroup::Row {
widgets: vec![TextLabel::new(description).multiline(true).widget_instance()],
},
LayoutGroup::row(vec![TextLabel::new("Graphite is free, open source software").bold(true).widget_instance()]),
LayoutGroup::row(vec![TextLabel::new(description).multiline(true).widget_instance()]),
])
}
}
@@ -12,7 +12,7 @@ impl DialogLayoutHolder for LicensesThirdPartyDialog {
fn layout_buttons(&self) -> Layout {
let widgets = vec![TextButton::new("OK").emphasized(true).on_update(|_| FrontendMessage::DialogClose.into()).widget_instance()];
Layout(vec![LayoutGroup::Row { widgets }])
Layout(vec![LayoutGroup::row(widgets)])
}
}
@@ -31,14 +31,12 @@ impl LayoutHolder for LicensesThirdPartyDialog {
// Two characters (one before, one after) the sequence of underscore characters, plus one additional column to provide a space between the text and the scrollbar
let non_wrapping_column_width = license_text.split('\n').map(|line| line.chars().filter(|&c| c == '_').count() as u32).max().unwrap_or(0) + 2 + 1;
Layout(vec![LayoutGroup::Row {
widgets: vec![
TextLabel::new(license_text)
.monospace(true)
.multiline(true)
.min_width_characters(non_wrapping_column_width)
.widget_instance(),
],
}])
Layout(vec![LayoutGroup::row(vec![
TextLabel::new(license_text)
.monospace(true)
.multiline(true)
.min_width_characters(non_wrapping_column_width)
.widget_instance(),
])])
}
}