From 58b6804f36462a77836ca6b103e6d8dba111441f Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Thu, 1 Oct 2026 14:02:54 -0400 Subject: [PATCH] =?UTF-8?q?M16.2:=20community=20links=20=E2=80=94=20Discor?= =?UTF-8?q?d,=20website,=20app=20page=20and=20GitHub?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Help menu (Join the ArtCraft Discord…, LightCraft Website, LightCraft on GitHub, ArtCraft Website), a one-click Discord button in the top bar (original chat-bubble icon), and links + a prominent Discord button in About. Opened through a new open_url service (desktop: system browser; web: new tab; https only). Co-Authored-By: Claude Opus 5.5 --- apps/lightcraft-web/src/web.rs | 4 +++ apps/lightcraft/src/main.rs | 13 ++++++++ crates/ui-egui/src/icons.rs | 22 +++++++++++++ crates/ui-egui/src/lib.rs | 5 +++ crates/ui-egui/src/links.rs | 48 ++++++++++++++++++++++++++++ crates/ui-egui/src/menubar.rs | 2 +- crates/ui-egui/src/menus.rs | 8 +++++ crates/ui-egui/src/panels/dialogs.rs | 20 ++++++++++++ crates/ui-egui/src/panels/topbar.rs | 1 + 9 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 crates/ui-egui/src/links.rs diff --git a/apps/lightcraft-web/src/web.rs b/apps/lightcraft-web/src/web.rs index a4247941..d9999840 100644 --- a/apps/lightcraft-web/src/web.rs +++ b/apps/lightcraft-web/src/web.rs @@ -159,6 +159,10 @@ fn services(originals: Originals, backend: Option, ctx: egui::Context) lightcraft_codecs::encode_png(&lightcraft_codecs::EncodeImage::rgba8(img), &lightcraft_codecs::EncodeMeta::default()).unwrap_or_default() })), reveal: None, + open_url: Some(Box::new(|url: &str| { + let w = web_sys::window().ok_or("no window")?; + w.open_with_url_and_target(url, "_blank").map(|_| ()).map_err(|_| "the browser blocked the new tab".to_string()) + })), pick_folder: None, } } diff --git a/apps/lightcraft/src/main.rs b/apps/lightcraft/src/main.rs index 7604b7c6..2ba700e0 100644 --- a/apps/lightcraft/src/main.rs +++ b/apps/lightcraft/src/main.rs @@ -82,6 +82,19 @@ fn save_prefs(app: &LightcraftApp) { fn services() -> Services { Services { pick_folder: Some(Box::new(|| rfd::FileDialog::new().set_title("Open Library").pick_folder().map(|p| p.to_string_lossy().to_string()))), + open_url: Some(Box::new(|url: &str| { + if !url.starts_with("https://") { + return Err("only https links are opened".into()); + } + let status = if cfg!(target_os = "macos") { + std::process::Command::new("open").arg(url).status() + } else if cfg!(target_os = "windows") { + std::process::Command::new("explorer").arg(url).status() + } else { + std::process::Command::new("xdg-open").arg(url).status() + }; + status.map(|_| ()).map_err(|e| e.to_string()) + })), reveal: Some(Box::new(|path: &str| { let status = if cfg!(target_os = "macos") { std::process::Command::new("open").args(["-R", path]).status() diff --git a/crates/ui-egui/src/icons.rs b/crates/ui-egui/src/icons.rs index c91ac650..2b5c09d5 100644 --- a/crates/ui-egui/src/icons.rs +++ b/crates/ui-egui/src/icons.rs @@ -71,6 +71,9 @@ pub enum Icon { Edited, /// Targeted adjustment: a ring with a centre dot and up/down arrows (drag vertically). Target, + /// Community chat (opens the ArtCraft Discord): a speech bubble with three dots. Our own + /// generic drawing, not any service's logo. + Chat, } struct Pen<'a> { @@ -431,5 +434,24 @@ pub fn paint(p: &Painter, rect: Rect, icon: Icon, color: Color32) { pen.line(&[(13.5, 5.5), (15.5, 3.5), (17.5, 5.5)]); pen.line(&[(13.5, 14.5), (15.5, 16.5), (17.5, 14.5)]); } + Chat => { + // rounded bubble with a tail at the lower left + pen.closed(&[ + (4.5, 3.5), + (15.5, 3.5), + (17.0, 5.0), + (17.0, 12.0), + (15.5, 13.5), + (9.0, 13.5), + (5.5, 16.8), + (5.5, 13.5), + (4.5, 13.5), + (3.0, 12.0), + (3.0, 5.0), + ]); + for x in [6.8, 10.0, 13.2] { + pen.dot(x, 8.5, 1.1); + } + } } } diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 5ae23829..4419a490 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -9,6 +9,7 @@ pub mod control; pub mod headless; pub mod icons; pub mod import; +pub mod links; pub mod menubar; pub mod menus; pub mod merge; @@ -40,6 +41,8 @@ pub type PngEncode = Box Vec>; pub type PickFolder = Box Option>; /// Reveal a file in the system file manager (Finder / Explorer / the folder on Linux). pub type RevealFn = Box Result<(), String>>; +/// Open a URL in the user's browser. +pub type OpenUrlFn = Box Result<(), String>>; /// Platform services injected by the host app (desktop or web). #[derive(Default)] @@ -57,6 +60,8 @@ pub struct Services { pub reveal: Option, /// Choose a folder (Settings → General → Open Library…; desktop only). pub pick_folder: Option, + /// Open a web link in the browser (Help menu, About, Discord button). + pub open_url: Option, } #[derive(Clone, Copy, Debug, Default)] diff --git a/crates/ui-egui/src/links.rs b/crates/ui-egui/src/links.rs new file mode 100644 index 00000000..e03b65c9 --- /dev/null +++ b/crates/ui-egui/src/links.rs @@ -0,0 +1,48 @@ +//! Community and project links (Help menu, About dialog, top-bar Discord button). + +/// The app's id in ArtCraft URLs. +pub const APP: &str = "lightcraft"; +/// The ArtCraft community Discord. +pub const DISCORD: &str = "https://discord.gg/artcraft"; +/// The ArtCraft website. +pub const WEBSITE: &str = "https://getartcraft.com"; +/// This app's page on the ArtCraft website. +pub const APP_PAGE: &str = "https://getartcraft.com/apps/lightcraft"; +/// This app's source repository. +pub const GITHUB: &str = "https://github.com/storytold/lightcraft"; + +/// (UI command id, menu label, URL) for each link, in Help-menu order. +pub const LINKS: &[(&str, &str, &str)] = &[ + ("app.discord", "Join the ArtCraft Discord…", DISCORD), + ("app.website", "LightCraft Website", APP_PAGE), + ("app.github", "LightCraft on GitHub", GITHUB), + ("app.artcraft", "ArtCraft Website", WEBSITE), +]; + +/// The URL behind a link command id. +pub fn url_of(cmd: &str) -> Option<&'static str> { + LINKS.iter().find(|(id, _, _)| *id == cmd).map(|(_, _, u)| *u) +} + +/// Open `url` in the user's browser (through the host's `open_url` service). +pub fn open(app: &mut crate::LightcraftApp, url: &str) -> Result { + let open = app.services.open_url.as_mut().ok_or("can't open links here")?; + open(url)?; + Ok(serde_json::json!({ "url": url })) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn links_are_https_and_name_this_app() { + for (id, label, url) in LINKS { + assert!(url.starts_with("https://"), "{id}"); + assert!(!label.is_empty()); + assert_eq!(url_of(id), Some(*url)); + } + assert!(APP_PAGE.ends_with(&format!("/apps/{APP}"))); + assert!(GITHUB.ends_with(&format!("/storytold/{APP}"))); + } +} diff --git a/crates/ui-egui/src/menubar.rs b/crates/ui-egui/src/menubar.rs index a5b0c89f..beb5cf64 100644 --- a/crates/ui-egui/src/menubar.rs +++ b/crates/ui-egui/src/menubar.rs @@ -182,7 +182,7 @@ const LAYOUT: &[(&str, &[&str])] = &[ "@Tools", ], ), - ("Help", &["app.shortcuts", "---", "app.about"]), + ("Help", &["app.discord", "---", "app.website", "app.github", "app.artcraft", "---", "app.shortcuts", "---", "app.about"]), ]; /// Registry entries that are reached another way (parameterized commands are expanded into diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 7934a113..765a1d48 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -89,6 +89,10 @@ pub const UI_COMMANDS: &[UiCommand] = &[ ("app.settings", "Settings…", Some("Cmd+,"), "Edit"), ("app.openLibrary", "Open Library…", None, "File"), ("app.about", "About LightCraft", None, "Help"), + ("app.discord", "Join the ArtCraft Discord…", None, "Help"), + ("app.website", "LightCraft Website", None, "Help"), + ("app.github", "LightCraft on GitHub", None, "Help"), + ("app.artcraft", "ArtCraft Website", None, "Help"), ("app.shortcuts", "Keyboard Shortcuts", Some("Cmd+/"), "Help"), ("app.export", "Export Now", None, ""), ("app.showInFinder", "Show in Finder", Some("Cmd+R"), "Photo"), @@ -602,6 +606,10 @@ pub fn run_ui_command(app: &mut LightcraftApp, id: &str, p: &Value) -> Option crate::control::export_active(app, p), "app.showInFinder" => show_in_finder(app), + "app.discord" | "app.website" | "app.github" | "app.artcraft" => { + let url = crate::links::url_of(id).unwrap_or(crate::links::WEBSITE); + crate::links::open(app, url) + } "app.exportPrevious" => match app.session.last_export.clone() { Some(prev) => crate::control::export_active(app, &prev), None => Err("nothing exported yet — use Export…".into()), diff --git a/crates/ui-egui/src/panels/dialogs.rs b/crates/ui-egui/src/panels/dialogs.rs index 718726b0..6c3ef6bf 100644 --- a/crates/ui-egui/src/panels/dialogs.rs +++ b/crates/ui-egui/src/panels/dialogs.rs @@ -365,6 +365,26 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) { ui.label(egui::RichText::new("LightCraft").font(t.semibold(20.0)).color(t.text)); ui.label(format!("Version {} — a clean-room, pure-Rust photo library and raw developer.", env!("CARGO_PKG_VERSION"))); ui.label("MIT OR Apache-2.0. Font: Inter (OFL). Icons: original."); + ui.add_space(10.0); + let discord = egui::Button::new(egui::RichText::new("Join the ArtCraft Discord").font(t.semibold(15.0)).color(egui::Color32::WHITE)) + .fill(t.accent) + .min_size(egui::vec2(260.0, 34.0)); + let r = ui.add(discord).on_hover_text(crate::links::DISCORD); + crate::widgets::register(ui.ctx(), "button:aboutDiscord", r.rect); + if r.clicked() { + let _ = crate::links::open(app, crate::links::DISCORD); + } + ui.add_space(6.0); + for (label, url) in [ + ("LightCraft website", crate::links::APP_PAGE), + ("Source code on GitHub", crate::links::GITHUB), + ("ArtCraft — more creative apps", crate::links::WEBSITE), + ] { + let r = ui.link(label).on_hover_text(url); + if r.clicked() { + let _ = crate::links::open(app, url); + } + } } Dialog::Shortcuts => { egui::ScrollArea::vertical().max_height(420.0).show(ui, |ui| { diff --git a/crates/ui-egui/src/panels/topbar.rs b/crates/ui-egui/src/panels/topbar.rs index 64899ee6..2958a9a5 100644 --- a/crates/ui-egui/src/panels/topbar.rs +++ b/crates/ui-egui/src/panels/topbar.rs @@ -97,6 +97,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { // right icons let mut x = full.right() - 18.0; for (id, icon, tip, cmd) in [ + ("discord", Icon::Chat, "Join the ArtCraft community on Discord", "app.discord"), ("cloud", Icon::Cloud, "Local library — no cloud account needed", ""), ("help", Icon::Help, "Keyboard shortcuts", "app.shortcuts"), ("share", Icon::Share, "Export", "dialog.export"),