Improve font import; replace Inconsolata with Source Code Pro; show third-party licenses in editor dialog (#3079)

* Improve font import; replace Inconsolata with Source Code Pro; show third-party licenses in editor dialog

* Code review
This commit is contained in:
Keavon Chambers 2025-08-21 11:57:04 -07:00 committed by GitHub
parent e56f858ced
commit 0e467907e2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 365 additions and 178 deletions

11
.vscode/settings.json vendored
View File

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

View File

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

View File

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

View File

@ -33,6 +33,9 @@ pub enum DialogMessage {
RequestLicensesDialogWithLocalizedCommitDate {
localized_commit_year: String,
},
RequestLicensesThirdPartyDialogWithLicenseText {
license_text: String,
},
RequestNewDocumentDialog,
RequestPreferencesDialog,
}

View File

@ -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<DialogMessage, DialogMessageContext<'_>> 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(),

View File

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

View File

@ -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")

View File

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

View File

@ -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(),
],
}]))
}
}

View File

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

View File

@ -64,6 +64,7 @@ pub enum FrontendMessage {
#[serde(rename = "commitDate")]
commit_date: String,
},
TriggerDisplayThirdPartyLicensesDialog,
TriggerSaveDocument {
document_id: DocumentId,
name: String,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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).

View File

@ -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 <input> elements
pointer-events: none;

View File

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

View File

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

View File

@ -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.

View File

@ -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<string, MessageMaker> = {
DisplayRemoveEditableTextbox,
SendUIMetadata,
TriggerAboutGraphiteLocalizedCommitDate,
TriggerDisplayThirdPartyLicensesDialog,
TriggerSaveDocument,
TriggerSaveFile,
TriggerExportImage,

View File

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

View File

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

View File

@ -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)]