diff --git a/.vscode/settings.json b/.vscode/settings.json index 8171f50c..1a8ced38 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -39,12 +39,11 @@ "eslint.validate": ["javascript", "typescript", "svelte"], // Svelte config "svelte.plugin.svelte.compilerWarnings": { - // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts` - "css-unused-selector": "ignore", - "vite-plugin-svelte-css-no-scopable-elements": "ignore", - "a11y-no-static-element-interactions": "ignore", - "a11y-no-noninteractive-element-interactions": "ignore", - "a11y-click-events-have-key-events": "ignore" + "css-unused-selector": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts` + "vite-plugin-svelte-css-no-scopable-elements": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts` + "a11y-no-static-element-interactions": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts` + "a11y-no-noninteractive-element-interactions": "ignore", // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts` + "a11y-click-events-have-key-events": "ignore" // NOTICE: Keep this list in sync with the list in `frontend/vite.config.ts` }, // VS Code config "html.format.wrapLineLength": 200, diff --git a/about.toml b/about.toml index e1ead699..7f9568c7 100644 --- a/about.toml +++ b/about.toml @@ -1,22 +1,22 @@ -# Keep this list in sync with those in `/deny.toml` and `/frontend/vite.config.ts`. accepted = [ - "Apache-2.0 WITH LLVM-exception", - "Apache-2.0", - "BSD-2-Clause", - "BSD-3-Clause", - "BSL-1.0", - "CC0-1.0", - "CDLA-Permissive-2.0", - "ISC", - "MIT-0", - "MIT", - "MPL-2.0", - "OpenSSL", - "Unicode-3.0", - "Unicode-DFS-2016", - "Zlib", - "NCSA", - "bzip2-1.0.6", + "Apache-2.0 WITH LLVM-exception", # Keep this list in sync with those in `/deny.toml` + "Apache-2.0", # Keep this list in sync with those in `/deny.toml` + "BSD-2-Clause", # Keep this list in sync with those in `/deny.toml` + "BSD-3-Clause", # Keep this list in sync with those in `/deny.toml` + "BSL-1.0", # Keep this list in sync with those in `/deny.toml` + "CC0-1.0", # Keep this list in sync with those in `/deny.toml` + "CDLA-Permissive-2.0", # Keep this list in sync with those in `/deny.toml` + "ISC", # Keep this list in sync with those in `/deny.toml` + "MIT-0", # Keep this list in sync with those in `/deny.toml` + "MIT", # Keep this list in sync with those in `/deny.toml` + "MPL-2.0", # Keep this list in sync with those in `/deny.toml` + "OpenSSL", # Keep this list in sync with those in `/deny.toml` + "Unicode-3.0", # Keep this list in sync with those in `/deny.toml` + "Unicode-DFS-2016", # Keep this list in sync with those in `/deny.toml` + "Zlib", # Keep this list in sync with those in `/deny.toml` + "NCSA", # Keep this list in sync with those in `/deny.toml` + "bzip2-1.0.6", # Keep this list in sync with those in `/deny.toml` + "OFL-1.1", # Keep this list in sync with those in `/deny.toml` ] workarounds = ["ring"] ignore-build-dependencies = true diff --git a/deny.toml b/deny.toml index 9f89b122..3ffe5838 100644 --- a/deny.toml +++ b/deny.toml @@ -63,25 +63,25 @@ ignore = [ # See https://spdx.org/licenses/ for list of possible licenses # [possible values: any SPDX 3.11 short identifier (+ optional exception)]. # -# Keep this list in sync with those in `/about.toml` and `/frontend/vite.config.ts`. allow = [ - "Apache-2.0 WITH LLVM-exception", - "Apache-2.0", - "BSD-2-Clause", - "BSD-3-Clause", - "BSL-1.0", - "CC0-1.0", - "CDLA-Permissive-2.0", - "ISC", - "MIT-0", - "MIT", - "MPL-2.0", - "OpenSSL", - "Unicode-3.0", - "Unicode-DFS-2016", - "Zlib", - "NCSA", - "bzip2-1.0.6", + "Apache-2.0 WITH LLVM-exception", # Keep this list in sync with those in `/about.toml` + "Apache-2.0", # Keep this list in sync with those in `/about.toml` + "BSD-2-Clause", # Keep this list in sync with those in `/about.toml` + "BSD-3-Clause", # Keep this list in sync with those in `/about.toml` + "BSL-1.0", # Keep this list in sync with those in `/about.toml` + "CC0-1.0", # Keep this list in sync with those in `/about.toml` + "CDLA-Permissive-2.0", # Keep this list in sync with those in `/about.toml` + "ISC", # Keep this list in sync with those in `/about.toml` + "MIT-0", # Keep this list in sync with those in `/about.toml` + "MIT", # Keep this list in sync with those in `/about.toml` + "MPL-2.0", # Keep this list in sync with those in `/about.toml` + "OpenSSL", # Keep this list in sync with those in `/about.toml` + "Unicode-3.0", # Keep this list in sync with those in `/about.toml` + "Unicode-DFS-2016", # Keep this list in sync with those in `/about.toml` + "Zlib", # Keep this list in sync with those in `/about.toml` + "NCSA", # Keep this list in sync with those in `/about.toml` + "bzip2-1.0.6", # Keep this list in sync with those in `/about.toml` + "OFL-1.1", # Keep this list in sync with those in `/about.toml` ] # The confidence threshold for detecting a license from license text. # The higher the value, the more closely the license text must be to the diff --git a/editor/src/messages/dialog/dialog_message.rs b/editor/src/messages/dialog/dialog_message.rs index baee1e65..435b83d8 100644 --- a/editor/src/messages/dialog/dialog_message.rs +++ b/editor/src/messages/dialog/dialog_message.rs @@ -33,6 +33,9 @@ pub enum DialogMessage { RequestLicensesDialogWithLocalizedCommitDate { localized_commit_year: String, }, + RequestLicensesThirdPartyDialogWithLicenseText { + license_text: String, + }, RequestNewDocumentDialog, RequestPreferencesDialog, } diff --git a/editor/src/messages/dialog/dialog_message_handler.rs b/editor/src/messages/dialog/dialog_message_handler.rs index 03e1f12e..42f17a22 100644 --- a/editor/src/messages/dialog/dialog_message_handler.rs +++ b/editor/src/messages/dialog/dialog_message_handler.rs @@ -1,5 +1,6 @@ use super::new_document_dialog::NewDocumentDialogMessageContext; use super::simple_dialogs::{self, AboutGraphiteDialog, ComingSoonDialog, DemoArtworkDialog, LicensesDialog}; +use crate::messages::dialog::simple_dialogs::LicensesThirdPartyDialog; use crate::messages::input_mapper::utility_types::input_mouse::ViewportBounds; use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::prelude::*; @@ -103,6 +104,10 @@ impl MessageHandler> for DialogMessageHa dialog.send_dialog_to_frontend(responses); } + DialogMessage::RequestLicensesThirdPartyDialogWithLicenseText { license_text } => { + let dialog = LicensesThirdPartyDialog { license_text }; + dialog.send_dialog_to_frontend(responses); + } DialogMessage::RequestNewDocumentDialog => { self.new_document_dialog = NewDocumentDialogMessageHandler { name: portfolio.generate_new_document_name(), diff --git a/editor/src/messages/dialog/export_dialog/export_dialog_message_handler.rs b/editor/src/messages/dialog/export_dialog/export_dialog_message_handler.rs index 4753279c..7f549999 100644 --- a/editor/src/messages/dialog/export_dialog/export_dialog_message_handler.rs +++ b/editor/src/messages/dialog/export_dialog/export_dialog_message_handler.rs @@ -92,13 +92,13 @@ impl LayoutHolder for ExportDialogMessageHandler { .collect(); let export_type = vec![ - TextLabel::new("File Type").table_align(true).min_width(100).widget_holder(), + TextLabel::new("File Type").table_align(true).min_width("100px").widget_holder(), Separator::new(SeparatorType::Unrelated).widget_holder(), RadioInput::new(entries).selected_index(Some(self.file_type as u32)).widget_holder(), ]; let resolution = vec![ - TextLabel::new("Scale Factor").table_align(true).min_width(100).widget_holder(), + TextLabel::new("Scale Factor").table_align(true).min_width("100px").widget_holder(), Separator::new(SeparatorType::Unrelated).widget_holder(), NumberInput::new(Some(self.scale_factor)) .unit("") @@ -144,14 +144,14 @@ impl LayoutHolder for ExportDialogMessageHandler { } let export_area = vec![ - TextLabel::new("Bounds").table_align(true).min_width(100).widget_holder(), + TextLabel::new("Bounds").table_align(true).min_width("100px").widget_holder(), Separator::new(SeparatorType::Unrelated).widget_holder(), DropdownInput::new(entries).selected_index(Some(index as u32)).widget_holder(), ]; let checkbox_id = CheckboxId::new(); let transparent_background = vec![ - TextLabel::new("Transparency").table_align(true).min_width(100).for_checkbox(checkbox_id).widget_holder(), + TextLabel::new("Transparency").table_align(true).min_width("100px").for_checkbox(checkbox_id).widget_holder(), Separator::new(SeparatorType::Unrelated).widget_holder(), CheckboxInput::new(self.transparent_background) .disabled(self.file_type == FileType::Jpg) diff --git a/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs b/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs index a9133024..5d3ae881 100644 --- a/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs +++ b/editor/src/messages/dialog/new_document_dialog/new_document_dialog_message_handler.rs @@ -79,7 +79,7 @@ impl DialogLayoutHolder for NewDocumentDialogMessageHandler { impl LayoutHolder for NewDocumentDialogMessageHandler { fn layout(&self) -> Layout { let name = vec![ - TextLabel::new("Name").table_align(true).min_width(90).widget_holder(), + TextLabel::new("Name").table_align(true).min_width("90px").widget_holder(), Separator::new(SeparatorType::Unrelated).widget_holder(), TextInput::new(&self.name) .on_update(|text_input: &TextInput| NewDocumentDialogMessage::Name { name: text_input.value.clone() }.into()) @@ -89,7 +89,7 @@ impl LayoutHolder for NewDocumentDialogMessageHandler { let checkbox_id = CheckboxId::new(); let infinite = vec![ - TextLabel::new("Infinite Canvas").table_align(true).min_width(90).for_checkbox(checkbox_id).widget_holder(), + TextLabel::new("Infinite Canvas").table_align(true).min_width("90px").for_checkbox(checkbox_id).widget_holder(), Separator::new(SeparatorType::Unrelated).widget_holder(), CheckboxInput::new(self.infinite) .on_update(|checkbox_input: &CheckboxInput| NewDocumentDialogMessage::Infinite { infinite: checkbox_input.checked }.into()) @@ -98,7 +98,7 @@ impl LayoutHolder for NewDocumentDialogMessageHandler { ]; let scale = vec![ - TextLabel::new("Dimensions").table_align(true).min_width(90).widget_holder(), + TextLabel::new("Dimensions").table_align(true).min_width("90px").widget_holder(), Separator::new(SeparatorType::Unrelated).widget_holder(), NumberInput::new(Some(self.dimensions.x as f64)) .label("W") diff --git a/editor/src/messages/dialog/simple_dialogs/licenses_dialog.rs b/editor/src/messages/dialog/simple_dialogs/licenses_dialog.rs index 694a9986..cebb43e9 100644 --- a/editor/src/messages/dialog/simple_dialogs/licenses_dialog.rs +++ b/editor/src/messages/dialog/simple_dialogs/licenses_dialog.rs @@ -16,22 +16,31 @@ impl DialogLayoutHolder for LicensesDialog { } fn layout_column_2(&self) -> Layout { - let icons_license_link = "https://raw.githubusercontent.com/GraphiteEditor/Graphite/master/frontend/assets/LICENSE.md"; - let links = [ - ("GraphiteLogo", "Graphite Logo", "https://graphite.rs/logo/"), - ("IconsGrid", "Graphite Icons", icons_license_link), - ("License", "Graphite License", "https://graphite.rs/license/"), - ("License", "Other Licenses", "/third-party-licenses.txt"), + #[allow(clippy::type_complexity)] + let button_definitions: &[(&str, &str, fn() -> Message)] = &[ + ("GraphiteLogo", "Graphite Logo", || { + FrontendMessage::TriggerVisitLink { + url: "https://graphite.rs/logo/".into(), + } + .into() + }), + ("IconsGrid", "Graphite Icons", || { + FrontendMessage::TriggerVisitLink { + url: "https://raw.githubusercontent.com/GraphiteEditor/Graphite/master/frontend/assets/LICENSE.md".into(), + } + .into() + }), + ("License", "Graphite License", || { + FrontendMessage::TriggerVisitLink { + url: "https://graphite.rs/license/".into(), + } + .into() + }), + ("License", "Other Licenses", || FrontendMessage::TriggerDisplayThirdPartyLicensesDialog.into()), ]; - let widgets = links - .into_iter() - .map(|(icon, label, url)| { - TextButton::new(label) - .icon(Some(icon.into())) - .flush(true) - .on_update(|_| FrontendMessage::TriggerVisitLink { url: url.into() }.into()) - .widget_holder() - }) + 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_holder()) .collect(); Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Column { widgets }])) diff --git a/editor/src/messages/dialog/simple_dialogs/licenses_third_party_dialog.rs b/editor/src/messages/dialog/simple_dialogs/licenses_third_party_dialog.rs new file mode 100644 index 00000000..4078f7a5 --- /dev/null +++ b/editor/src/messages/dialog/simple_dialogs/licenses_third_party_dialog.rs @@ -0,0 +1,44 @@ +use crate::messages::layout::utility_types::widget_prelude::*; +use crate::messages::prelude::*; + +pub struct LicensesThirdPartyDialog { + pub license_text: String, +} + +impl DialogLayoutHolder for LicensesThirdPartyDialog { + const ICON: &'static str = "License12px"; + const TITLE: &'static str = "Third-Party Software License Notices"; + + fn layout_buttons(&self) -> Layout { + let widgets = vec![TextButton::new("OK").emphasized(true).on_update(|_| FrontendMessage::DisplayDialogDismiss.into()).widget_holder()]; + + Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }])) + } +} + +impl LayoutHolder for LicensesThirdPartyDialog { + fn layout(&self) -> Layout { + // Remove the header and begin with the line containing the first license section (we otherwise keep the title for standalone viewing of the licenses text file) + let license_text = if let Some(first_underscore_line) = self.license_text.lines().position(|line| line.contains('_')) { + // Find the byte position where the line with underscore starts + let char_position = self.license_text.split('\n').take(first_underscore_line).map(|line| line.len() + '\n'.len_utf8()).sum(); + self.license_text[char_position..].to_string() + } else { + // This shouldn't be encountered, but if no underscore line is found, we use the full text as a safety fallback + self.license_text.clone() + }; + + // 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()).max().unwrap_or(0) + 2 + 1; + + Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { + widgets: vec![ + TextLabel::new(license_text) + .monospace(true) + .multiline(true) + .min_width(format!("{non_wrapping_column_width}ch")) + .widget_holder(), + ], + }])) + } +} diff --git a/editor/src/messages/dialog/simple_dialogs/mod.rs b/editor/src/messages/dialog/simple_dialogs/mod.rs index a330efac..181b3bf1 100644 --- a/editor/src/messages/dialog/simple_dialogs/mod.rs +++ b/editor/src/messages/dialog/simple_dialogs/mod.rs @@ -5,6 +5,7 @@ mod coming_soon_dialog; mod demo_artwork_dialog; mod error_dialog; mod licenses_dialog; +mod licenses_third_party_dialog; pub use about_graphite_dialog::AboutGraphiteDialog; pub use close_all_documents_dialog::CloseAllDocumentsDialog; @@ -14,3 +15,4 @@ pub use demo_artwork_dialog::ARTWORK; pub use demo_artwork_dialog::DemoArtworkDialog; pub use error_dialog::ErrorDialog; pub use licenses_dialog::LicensesDialog; +pub use licenses_third_party_dialog::LicensesThirdPartyDialog; diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index 1289fc4d..50b75536 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -64,6 +64,7 @@ pub enum FrontendMessage { #[serde(rename = "commitDate")] commit_date: String, }, + TriggerDisplayThirdPartyLicensesDialog, TriggerSaveDocument { document_id: DocumentId, name: String, diff --git a/editor/src/messages/layout/utility_types/widgets/label_widgets.rs b/editor/src/messages/layout/utility_types/widgets/label_widgets.rs index cb569730..174bc586 100644 --- a/editor/src/messages/layout/utility_types/widgets/label_widgets.rs +++ b/editor/src/messages/layout/utility_types/widgets/label_widgets.rs @@ -44,16 +44,18 @@ pub struct TextLabel { pub italic: bool, + pub monospace: bool, + + pub multiline: bool, + #[serde(rename = "centerAlign")] pub center_align: bool, #[serde(rename = "tableAlign")] pub table_align: bool, - pub multiline: bool, - #[serde(rename = "minWidth")] - pub min_width: u32, + pub min_width: String, pub tooltip: String, diff --git a/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs b/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs index 584c64b0..479f6a37 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs @@ -1021,6 +1021,8 @@ impl OverlayContextInternal { }; // Load Source Sans Pro font data + // TODO: Grab this from the node_modules folder (either with `include_bytes!` or ideally at runtime) instead of checking the font file into the repo. + // TODO: And maybe use the WOFF2 version (if it's supported) for its smaller, compressed file size. const FONT_DATA: &[u8] = include_bytes!("source-sans-pro-regular.ttf"); let font_blob = Some(load_font(FONT_DATA)); @@ -1046,6 +1048,8 @@ impl OverlayContextInternal { }; // Load Source Sans Pro font data + // TODO: Grab this from the node_modules folder (either with `include_bytes!` or ideally at runtime) instead of checking the font file into the repo. + // TODO: And maybe use the WOFF2 version (if it's supported) for its smaller, compressed file size. const FONT_DATA: &[u8] = include_bytes!("source-sans-pro-regular.ttf"); let font_blob = Some(load_font(FONT_DATA)); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3d12766c..2c4ec1da 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -7,11 +7,11 @@ "name": "graphite-web-frontend", "license": "Apache-2.0", "dependencies": { - "@fontsource/inconsolata": "^5.2.5", - "@fontsource/source-sans-pro": "^5.2.5", "class-transformer": "^0.5.1", "idb-keyval": "^6.2.1", - "reflect-metadata": "^0.2.2" + "reflect-metadata": "^0.2.2", + "source-code-pro": "github:adobe-fonts/source-code-pro#2.042R-u/1.062R-i/1.026R-vf", + "source-sans": "github:adobe-fonts/source-sans#2.045R-ro%2F1.095R-it" }, "devDependencies": { "@sveltejs/vite-plugin-svelte": "^3.1.2", @@ -29,7 +29,7 @@ "prettier": "^3.3.3", "prettier-plugin-svelte": "^3.2.6", "process": "^0.11.10", - "rollup-plugin-license": "^3.5.3", + "rollup-plugin-license": "^3.6.0", "sass": "1.78.0", "svelte": "^4.2.19", "svelte-preprocess": "^6.0.2", @@ -556,21 +556,6 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/@fontsource/inconsolata": { - "version": "5.2.5", - "resolved": "https://registry.npmjs.org/@fontsource/inconsolata/-/inconsolata-5.2.5.tgz", - "integrity": "sha512-OvzkZY5qYghv/jEV6cfGZzFhdFTvSnU+ExPC7WcZ7w8PdRhtiu/SpcBWOBt+3LXgS0n9qyepgq4zZmxlDTlGGQ==", - "license": "OFL-1.1", - "funding": { - "url": "https://github.com/sponsors/ayuhito" - } - }, - "node_modules/@fontsource/source-sans-pro": { - "version": "5.2.5", - "resolved": "https://registry.npmjs.org/@fontsource/source-sans-pro/-/source-sans-pro-5.2.5.tgz", - "integrity": "sha512-ypendqc4pYUc+EgF7qqPY9iVYEz1t/Qr03VojKxG/2g3dnpHa1B6DOlDxWQjQXDj5QrG6inEqGT0g+edjALZyg==", - "license": "OFL-1.1" - }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -4622,14 +4607,14 @@ } }, "node_modules/rollup-plugin-license": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/rollup-plugin-license/-/rollup-plugin-license-3.5.3.tgz", - "integrity": "sha512-r3wImZSo2d6sEk9BRJtlzeI/upjyjnpthy06Fdl0EzqRrlg3ULb9KQR7xHJI0zuayW/8bchEXSF5dO6dha4OyA==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-license/-/rollup-plugin-license-3.6.0.tgz", + "integrity": "sha512-1ieLxTCaigI5xokIfszVDRoy6c/Wmlot1fDEnea7Q/WXSR8AqOjYljHDLObAx7nFxHC2mbxT3QnTSPhaic2IYw==", "dev": true, "license": "MIT", "dependencies": { "commenting": "~1.1.0", - "fdir": "6.3.0", + "fdir": "^6.4.3", "lodash": "~4.17.21", "magic-string": "~0.30.0", "moment": "~2.30.1", @@ -4645,11 +4630,14 @@ } }, "node_modules/rollup-plugin-license/node_modules/fdir": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.3.0.tgz", - "integrity": "sha512-QOnuT+BOtivR77wYvCWHfGt9s4Pz1VIMbD463vegT5MLqNXy8rYFT/lPVEqf/bhYeT6qmqrNHhsX+rWwe3rOCQ==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, "peerDependencies": { "picomatch": "^3 || ^4" }, @@ -4660,9 +4648,9 @@ } }, "node_modules/rollup-plugin-license/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "optional": true, @@ -4864,6 +4852,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/source-code-pro": { + "version": "2.38.0", + "resolved": "git+ssh://git@github.com/adobe-fonts/source-code-pro.git#d3f1a5962cde503f9409c21e58527611d4a19ef1", + "license": "OFL-1.1" + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -4874,6 +4867,12 @@ "node": ">=0.10.0" } }, + "node_modules/source-sans": { + "name": "source-sans-pro", + "version": "2.40.0", + "resolved": "git+ssh://git@github.com/adobe-fonts/source-sans.git#ce77773581f4d454f0fa985c073bb25c721bfcf5", + "license": "SIL Open Font License 1.1" + }, "node_modules/spdx-compare": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/spdx-compare/-/spdx-compare-1.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index a5424935..02eb2e41 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -31,11 +31,11 @@ "wasm:watch-production": "cargo watch --postpone --watch-when-idle --workdir=wasm --shell \"wasm-pack build . --release --target=web -- --color=always\"" }, "dependencies": { - "@fontsource/inconsolata": "^5.2.5", - "@fontsource/source-sans-pro": "^5.2.5", "class-transformer": "^0.5.1", "idb-keyval": "^6.2.1", - "reflect-metadata": "^0.2.2" + "reflect-metadata": "^0.2.2", + "source-code-pro": "github:adobe-fonts/source-code-pro#2.042R-u/1.062R-i/1.026R-vf", + "source-sans": "github:adobe-fonts/source-sans#2.045R-ro%2F1.095R-it" }, "devDependencies": { "@sveltejs/vite-plugin-svelte": "^3.1.2", @@ -53,7 +53,7 @@ "prettier": "^3.3.3", "prettier-plugin-svelte": "^3.2.6", "process": "^0.11.10", - "rollup-plugin-license": "^3.5.3", + "rollup-plugin-license": "^3.6.0", "sass": "1.78.0", "svelte": "^4.2.19", "svelte-preprocess": "^6.0.2", diff --git a/frontend/src/components/Editor.svelte b/frontend/src/components/Editor.svelte index fb334c4a..a9b71310 100644 --- a/frontend/src/components/Editor.svelte +++ b/frontend/src/components/Editor.svelte @@ -352,4 +352,44 @@ :not(.optional-input) > .checkbox-input input:focus-visible + label.checked { outline: 1px dashed var(--color-2-mildblack); } + + @font-face { + font-family: "Source Sans Pro"; + font-weight: 400; + font-style: normal; + font-stretch: normal; + src: url("@graphite/../node_modules/source-sans/WOFF2/TTF/SourceSansPro-Regular.ttf.woff2") format("woff2"); + } + + @font-face { + font-family: "Source Sans Pro"; + font-weight: 400; + font-style: italic; + font-stretch: normal; + src: url("@graphite/../node_modules/source-sans/WOFF2/TTF/SourceSansPro-It.ttf.woff2") format("woff2"); + } + + @font-face { + font-family: "Source Sans Pro"; + font-weight: 700; + font-style: normal; + font-stretch: normal; + src: url("@graphite/../node_modules/source-sans/WOFF2/TTF/SourceSansPro-Bold.ttf.woff2") format("woff2"); + } + + @font-face { + font-family: "Source Sans Pro"; + font-weight: 700; + font-style: italic; + font-stretch: normal; + src: url("@graphite/../node_modules/source-sans/WOFF2/TTF/SourceSansPro-BoldIt.ttf.woff2") format("woff2"); + } + + @font-face { + font-family: "Source Code Pro"; + font-weight: 400; + font-style: normal; + font-stretch: normal; + src: url("@graphite/../node_modules/source-code-pro/WOFF2/TTF/SourceCodePro-Regular.ttf.woff2") format("woff2"); + } diff --git a/frontend/src/components/floating-menus/Dialog.svelte b/frontend/src/components/floating-menus/Dialog.svelte index e9d0ba5f..928d8699 100644 --- a/frontend/src/components/floating-menus/Dialog.svelte +++ b/frontend/src/components/floating-menus/Dialog.svelte @@ -89,6 +89,7 @@ .header-area, .footer-area { background: var(--color-1-nearblack); + flex: 0 0 auto; } .header-area, @@ -113,6 +114,8 @@ .content { margin: -4px 0; + padding-right: calc(24px + 1px * var(--even-integer-subpixel-expansion-x)); + padding-bottom: calc(16px + 1px * var(--even-integer-subpixel-expansion-y)); &.center .row { justify-content: center; @@ -126,18 +129,22 @@ } } - .details.text-label { - -webkit-user-select: text; // Required as of Safari 15.0 (Graphite's minimum version) through the latest release - user-select: text; - white-space: pre-wrap; - max-width: 400px; - height: auto; - } - .radio-input button { flex-grow: 1; } + .text-label.multiline { + -webkit-user-select: text; // Still required by Safari as of 2025 + user-select: text; + } + + // Used by the "Third-Party Software License Notices" dialog + .details:has(.text-label.multiline.monospace) { + max-height: 60vh; + max-width: 80vw; + overflow: auto; + } + // Used by the "Open Demo Artwork" dialog .image-label { border-radius: 2px; diff --git a/frontend/src/components/layout/FloatingMenu.svelte b/frontend/src/components/layout/FloatingMenu.svelte index e4070cb1..6c18cf51 100644 --- a/frontend/src/components/layout/FloatingMenu.svelte +++ b/frontend/src/components/layout/FloatingMenu.svelte @@ -137,20 +137,23 @@ // This solves antialiasing issues when the content isn't cleanly divisible by 2 and gets translated by (-50%, -50%) causing all its content to be blurry. const floatingMenuContentDiv = floatingMenuContent?.div?.(); if (type === "Dialog" && floatingMenuContentDiv) { - // TODO: Also use https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver to detect any changes which may affect the size of the content. - // TODO: The current method only notices when the dialog size increases but can't detect when it decreases. const resizeObserver = new ResizeObserver((entries) => { entries.forEach((entry) => { - let { width, height } = entry.contentRect; + const existingWidth = Number(floatingMenuContentDiv.style.getPropertyValue("--even-integer-subpixel-expansion-x")); + const existingHeight = Number(floatingMenuContentDiv.style.getPropertyValue("--even-integer-subpixel-expansion-y")); - width = Math.ceil(width); - if (width % 2 === 1) width += 1; - height = Math.ceil(height); - if (height % 2 === 1) height += 1; + let { width, height } = entry.contentRect; + width -= existingWidth; + height -= existingHeight; + + let targetWidth = Math.ceil(width); + if (targetWidth % 2 === 1) targetWidth += 1; + let targetHeight = Math.ceil(height); + if (targetHeight % 2 === 1) targetHeight += 1; // We have to set the style properties directly because attempting to do it through a Svelte bound property results in `afterUpdate()` being triggered - floatingMenuContentDiv.style.setProperty("min-width", width === 0 ? "unset" : `${width}px`); - floatingMenuContentDiv.style.setProperty("min-height", height === 0 ? "unset" : `${height}px`); + floatingMenuContentDiv.style.setProperty("--even-integer-subpixel-expansion-x", `${targetWidth - width}`); + floatingMenuContentDiv.style.setProperty("--even-integer-subpixel-expansion-y", `${targetHeight - height}`); }); }); resizeObserver.observe(floatingMenuContentDiv); @@ -158,14 +161,6 @@ }); afterUpdate(() => { - // Remove the size constraint after the content updates so the resize observer can measure the content and reapply a newly calculated one - const floatingMenuContentDiv = floatingMenuContent?.div?.(); - if (type === "Dialog" && floatingMenuContentDiv) { - // We have to set the style properties directly because attempting to do it through a Svelte bound property results in `afterUpdate()` being triggered - floatingMenuContentDiv.style.setProperty("min-width", "unset"); - floatingMenuContentDiv.style.setProperty("min-height", "unset"); - } - // Gets the client bounds of the elements and apply relevant styles to them. // TODO: Use DOM attribute bindings more whilst not causing recursive updates. Turning measuring on and off both causes the component to change, // TODO: which causes the `afterUpdate()` Svelte event to fire extraneous times (hurting performance and sometimes causing an infinite loop). diff --git a/frontend/src/components/panels/Layers.svelte b/frontend/src/components/panels/Layers.svelte index 2f362679..2fd35017 100644 --- a/frontend/src/components/panels/Layers.svelte +++ b/frontend/src/components/panels/Layers.svelte @@ -770,7 +770,7 @@ width: 100%; &:disabled { - -webkit-user-select: none; // Required as of Safari 15.0 (Graphite's minimum version) through the latest release + -webkit-user-select: none; // Still required by Safari as of 2025 user-select: none; // Workaround for `user-select: none` not working on elements pointer-events: none; diff --git a/frontend/src/components/widgets/labels/TextLabel.svelte b/frontend/src/components/widgets/labels/TextLabel.svelte index 4595c6f2..ca845047 100644 --- a/frontend/src/components/widgets/labels/TextLabel.svelte +++ b/frontend/src/components/widgets/labels/TextLabel.svelte @@ -8,9 +8,10 @@ export let disabled = false; export let bold = false; export let italic = false; + export let monospace = false; export let centerAlign = false; export let tableAlign = false; - export let minWidth = 0; + export let minWidth = ""; export let multiline = false; export let tooltip: string | undefined = undefined; export let forCheckbox: bigint | undefined = undefined; @@ -28,10 +29,11 @@ class:disabled class:bold class:italic + class:monospace class:multiline class:center-align={centerAlign} class:table-align={tableAlign} - style:min-width={minWidth > 0 ? `${minWidth}px` : undefined} + style:min-width={minWidth || undefined} style={`${styleName} ${extraStyles}`.trim() || undefined} title={tooltip} for={forCheckbox !== undefined ? `checkbox-input-${forCheckbox}` : undefined} @@ -58,6 +60,11 @@ font-style: italic; } + &.monospace { + font-family: "Source Code Pro", monospace; + font-size: 12px; + } + &.multiline { white-space: pre-wrap; margin: 4px 0; diff --git a/frontend/src/components/widgets/labels/UserInputLabel.svelte b/frontend/src/components/widgets/labels/UserInputLabel.svelte index f19d99d5..863ec33e 100644 --- a/frontend/src/components/widgets/labels/UserInputLabel.svelte +++ b/frontend/src/components/widgets/labels/UserInputLabel.svelte @@ -183,7 +183,8 @@ display: flex; justify-content: center; align-items: center; - font-family: "Inconsolata", monospace; + font-family: "Source Code Pro", monospace; + font-size: 12px; font-weight: 400; text-align: center; height: 16px; diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 8ab407bb..1e46fa61 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -1,12 +1,5 @@ // This file is the browser's entry point for the JS bundle -// Fonts -import "@fontsource/inconsolata"; -import "@fontsource/source-sans-pro/400-italic.css"; -import "@fontsource/source-sans-pro/400.css"; -import "@fontsource/source-sans-pro/700-italic.css"; -import "@fontsource/source-sans-pro/700.css"; - // `reflect-metadata` allows for runtime reflection of types in JavaScript. // It is needed for class-transformer to work and is imported as a side effect. // The library replaces the Reflect API on the window to support more features. diff --git a/frontend/src/messages.ts b/frontend/src/messages.ts index 3a4a256e..df4196ce 100644 --- a/frontend/src/messages.ts +++ b/frontend/src/messages.ts @@ -946,6 +946,8 @@ export class TriggerAboutGraphiteLocalizedCommitDate extends JsMessage { readonly commitDate!: string; } +export class TriggerDisplayThirdPartyLicensesDialog extends JsMessage {} + // WIDGET PROPS export abstract class WidgetProps { @@ -1370,13 +1372,15 @@ export class TextLabel extends WidgetProps { italic!: boolean; + monospace!: boolean; + + multiline!: boolean; + centerAlign!: boolean; tableAlign!: boolean; - minWidth!: number; - - multiline!: boolean; + minWidth!: string; @Transform(({ value }: { value: string }) => value || undefined) tooltip!: string | undefined; @@ -1692,6 +1696,7 @@ export const messageMakers: Record = { DisplayRemoveEditableTextbox, SendUIMetadata, TriggerAboutGraphiteLocalizedCommitDate, + TriggerDisplayThirdPartyLicensesDialog, TriggerSaveDocument, TriggerSaveFile, TriggerExportImage, diff --git a/frontend/src/state-providers/dialog.ts b/frontend/src/state-providers/dialog.ts index 1e2da232..57a6c7e5 100644 --- a/frontend/src/state-providers/dialog.ts +++ b/frontend/src/state-providers/dialog.ts @@ -1,7 +1,16 @@ import { writable } from "svelte/store"; import { type Editor } from "@graphite/editor"; -import { defaultWidgetLayout, DisplayDialog, DisplayDialogDismiss, UpdateDialogButtons, UpdateDialogColumn1, UpdateDialogColumn2, patchWidgetLayout } from "@graphite/messages"; +import { + defaultWidgetLayout, + DisplayDialog, + DisplayDialogDismiss, + UpdateDialogButtons, + UpdateDialogColumn1, + UpdateDialogColumn2, + patchWidgetLayout, + TriggerDisplayThirdPartyLicensesDialog, +} from "@graphite/messages"; import { type IconName } from "@graphite/utility-functions/icons"; // eslint-disable-next-line @typescript-eslint/explicit-function-return-type @@ -78,6 +87,17 @@ export function createDialogState(editor: Editor) { }); editor.subscriptions.subscribeJsMessage(DisplayDialogDismiss, dismissDialog); + editor.subscriptions.subscribeJsMessage(TriggerDisplayThirdPartyLicensesDialog, async () => { + const BACKUP_URL = "https://editor.graphite.rs/third-party-licenses.txt"; + let licenseText = `Content was not able to load. Please check your network connection and try again.\n\nOr visit ${BACKUP_URL} for the license notices.`; + if (editor.handle.inDevelopmentMode()) licenseText = `Third-party licenses are not available in development builds.\n\nVisit ${BACKUP_URL} for the license notices.`; + + const response = await fetch("/third-party-licenses.txt"); + if (response.ok && response.headers.get("Content-Type")?.includes("text/plain")) licenseText = await response.text(); + + editor.handle.requestLicensesThirdPartyDialogWithLicenseText(licenseText); + }); + return { subscribe, dismissDialog, diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 2d76e943..bc15f2b0 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -13,34 +13,19 @@ import { DynamicPublicDirectory as viteMultipleAssets } from "vite-multiple-asse const projectRootDir = path.resolve(__dirname); -// Keep this list in sync with those in `/about.toml` and `/deny.toml`. -const ALLOWED_LICENSES = [ - "Apache-2.0 WITH LLVM-exception", - "Apache-2.0", - "BSD-2-Clause", - "BSD-3-Clause", - "BSL-1.0", - "CC0-1.0", - "CDLA-Permissive-2.0", - "ISC", - "MIT-0", - "MIT", - "MPL-2.0", - "OpenSSL", - "Unicode-3.0", - "Unicode-DFS-2016", - "Zlib", - "NCSA", -]; - // https://vitejs.dev/config/ export default defineConfig({ plugins: [ svelte({ preprocess: [sveltePreprocess()], onwarn(warning, defaultHandler) { - // NOTICE: Keep this list in sync with the list in `.vscode/settings.json` - const suppressed = ["css-unused-selector", "vite-plugin-svelte-css-no-scopable-elements", "a11y-no-static-element-interactions", "a11y-no-noninteractive-element-interactions"]; + const suppressed = [ + "css-unused-selector", // NOTICE: Keep this list in sync with the list in `.vscode/settings.json` + "vite-plugin-svelte-css-no-scopable-elements", // NOTICE: Keep this list in sync with the list in `.vscode/settings.json` + "a11y-no-static-element-interactions", // NOTICE: Keep this list in sync with the list in `.vscode/settings.json` + "a11y-no-noninteractive-element-interactions", // NOTICE: Keep this list in sync with the list in `.vscode/settings.json` + "a11y-click-events-have-key-events", // NOTICE: Keep this list in sync with the list in `.vscode/settings.json` + ]; if (suppressed.includes(warning.code)) return; defaultHandler?.(warning); @@ -66,8 +51,10 @@ export default defineConfig({ plugins: [ rollupPluginLicense({ thirdParty: { + includePrivate: false, + multipleVersions: true, allow: { - test: `(${ALLOWED_LICENSES.join(" OR ")})`, + test: `(${getAcceptedLicenses()})`, failOnUnlicensed: true, failOnViolation: true, }, @@ -103,10 +90,11 @@ type PackageInfo = { function formatThirdPartyLicenses(jsLicenses: Dependency[]): string { // Generate the Rust license information. - let licenses = generateRustLicenses() || []; + const rustLicenses = generateRustLicenses(); + const additionalLicenses = generateAdditionalLicenses(); - // Ensure we have license information to work with before proceeding. - if (licenses.length === 0) { + // Ensure we have the required license information to work with before proceeding. + if (rustLicenses.length === 0) { // This is probably caused by `cargo about` not being installed. console.error("Could not run `cargo about`, which is required to generate license information."); console.error("To install cargo-about on your system, you can run `cargo install cargo-about`."); @@ -121,9 +109,11 @@ function formatThirdPartyLicenses(jsLicenses: Dependency[]): string { process.exit(1); } - // Find then duplicate this license if one of its packages is `path-bool`, adding its notice text. - let foundLicensesIndex; - let foundPackagesIndex; + let licenses = rustLicenses.concat(additionalLicenses); + + // SPECIAL CASE: Find then duplicate this license if one of its packages is `path-bool`, adding its notice text. + let foundLicensesIndex: number | undefined = undefined; + let foundPackagesIndex: number | undefined = undefined; licenses.forEach((license, licenseIndex) => { license.packages.forEach((pkg, pkgIndex) => { if (pkg.name === "path-bool") { @@ -147,7 +137,7 @@ function formatThirdPartyLicenses(jsLicenses: Dependency[]): string { }); } - // Augment the imported Rust license list with the provided JS license list. + // Extend the license list with the provided JS licenses. jsLicenses.forEach((jsLicense) => { const name = jsLicense.name || ""; const version = jsLicense.version || ""; @@ -158,14 +148,12 @@ function formatThirdPartyLicenses(jsLicenses: Dependency[]): string { let repository = jsLicense.repository || ""; if (repository && typeof repository === "object") repository = repository.url; - // Remove the `git+` or `git://` prefix and `.git` suffix. - const repo = repository ? repository.replace(/^.*(github.com\/.*?\/.*?)(?:.git)/, "https://$1") : repository; const matchedLicense = licenses.find( (license) => license.licenseName === licenseName && trimBlankLines(license.licenseText || "") === licenseText && trimBlankLines(license.noticeText || "") === noticeText, ); - const pkg: PackageInfo = { name, version, author, repository: repo }; + const pkg: PackageInfo = { name, version, author, repository }; if (matchedLicense) matchedLicense.packages.push(pkg); else licenses.push({ licenseName, licenseText, noticeText, packages: [pkg] }); }); @@ -232,7 +220,15 @@ function formatThirdPartyLicenses(jsLicenses: Dependency[]): string { licenses.forEach((license) => { let packagesWithSameLicense = license.packages.map((packageInfo) => { const { name, version, author, repository } = packageInfo; - return `${name} ${version}${author ? ` - ${author}` : ""}${repository ? ` - ${repository}` : ""}`; + + // Remove the `git+` or `git://` prefix and `.git` suffix. + let repo = repository; + if (repo.startsWith("git+")) repo = repo.slice("git+".length); + if (repo.startsWith("git://")) repo = repo.slice("git://".length); + if (repo.endsWith(".git")) repo = repo.slice(0, -".git".length); + if (repo.endsWith(".git#release")) repo = repo.slice(0, -".git#release".length); + + return `${name} ${version}${author ? ` - ${author}` : ""}${repo ? ` - ${repo}` : ""}`; }); const multi = packagesWithSameLicense.length !== 1; const saysLicense = license.licenseName.toLowerCase().includes("license"); @@ -249,10 +245,44 @@ function formatThirdPartyLicenses(jsLicenses: Dependency[]): string { formattedLicenseNotice += ` ${"‾".repeat(packagesLineLength + 2)}\n`; formattedLicenseNotice += `${license.licenseText}\n`; }); + + formattedLicenseNotice += "\n"; return formattedLicenseNotice; } -function generateRustLicenses(): LicenseInfo[] | undefined { +// Include additional licenses that aren't automatically generated by `cargo about` or `rollup-plugin-license`. +function generateAdditionalLicenses(): LicenseInfo[] { + const ADDITIONAL_LICENSES = [ + { + licenseName: "SIL Open Font License 1.1", + licenseTextPath: "node_modules/source-sans/LICENSE.txt", + manifestPath: "node_modules/source-sans/package.json", + }, + { + licenseName: "SIL Open Font License 1.1", + licenseTextPath: "node_modules/source-code-pro/LICENSE.md", + manifestPath: "node_modules/source-code-pro/package.json", + }, + ]; + + return ADDITIONAL_LICENSES.map(({ licenseName, licenseTextPath, manifestPath }) => { + const licenseText = (fs.existsSync(licenseTextPath) && fs.readFileSync(licenseTextPath, "utf8")) || ""; + + const manifestJSON = (fs.existsSync(manifestPath) && JSON.parse(fs.readFileSync(manifestPath, "utf8"))) || {}; + const name = manifestJSON.name || ""; + const version = manifestJSON.version || ""; + const author = manifestJSON.author.name || manifestJSON.author || ""; + const repository = manifestJSON.repository?.url || ""; + + return { + licenseName, + licenseText: trimBlankLines(licenseText), + packages: [{ name, version, author, repository }], + }; + }); +} + +function generateRustLicenses(): LicenseInfo[] { // Log the starting status to the build output. console.info("\n\nGenerating license information for Rust code\n"); @@ -272,14 +302,14 @@ function generateRustLicenses(): LicenseInfo[] | undefined { if (status !== 101) { console.error("cargo-about failed", status, stderr); } - return undefined; + return []; } // Make sure the output starts with this expected label, which lets us know the file generated with expected output. // We don't want to eval an error message or something else, so we fail early if that happens. if (!stdout.trim().startsWith("GENERATED_BY_CARGO_ABOUT:")) { console.error("Unexpected output from cargo-about", stdout); - return undefined; + return []; } // Convert the array JS syntax string into an actual JS array in memory. @@ -308,7 +338,7 @@ function generateRustLicenses(): LicenseInfo[] | undefined { return rustLicenses; } catch (_) { - return undefined; + return []; } } @@ -355,3 +385,18 @@ function trimBlankLines(input: string): string { return result; } + +function getAcceptedLicenses() { + const tomlContent = fs.readFileSync(path.resolve(__dirname, "../about.toml"), "utf8"); + + const licensesBlock = tomlContent?.match(/accepted\s*=\s*\[([^\]]*)\]/)?.[1] || ""; + + return licensesBlock + .split("\n") + .map((line) => line.replace(/#.*$/, "")) // Remove comments + .join("\n") + .split(",") + .map((license) => license.trim().replace(/"/g, "")) + .filter((license) => license.length > 0) + .join(" OR "); +} diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index 34983bd2..86dae3a9 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -466,6 +466,12 @@ impl EditorHandle { self.dispatch(message); } + #[wasm_bindgen(js_name = requestLicensesThirdPartyDialogWithLicenseText)] + pub fn request_licenses_third_party_dialog_with_license_text(&self, license_text: String) { + let message = DialogMessage::RequestLicensesThirdPartyDialogWithLicenseText { license_text }; + self.dispatch(message); + } + /// Send new bounds when document panel viewports get resized or moved within the editor /// [left, top, right, bottom]... #[wasm_bindgen(js_name = boundsOfViewports)]