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
26 changed files with 365 additions and 178 deletions
@@ -33,6 +33,9 @@ pub enum DialogMessage {
RequestLicensesDialogWithLocalizedCommitDate {
localized_commit_year: String,
},
RequestLicensesThirdPartyDialogWithLicenseText {
license_text: String,
},
RequestNewDocumentDialog,
RequestPreferencesDialog,
}
@@ -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(),
@@ -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)
@@ -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")
@@ -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 }]))
@@ -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(),
],
}]))
}
}
@@ -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;
@@ -64,6 +64,7 @@ pub enum FrontendMessage {
#[serde(rename = "commitDate")]
commit_date: String,
},
TriggerDisplayThirdPartyLicensesDialog,
TriggerSaveDocument {
document_id: DocumentId,
name: String,
@@ -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,
@@ -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));