Community links: Help menu (Discord, ArtCraft website, app page, GitHub), Discord button in the header, links on the About and Home screens; engine help.* commands return the URLs (menu clicks open them via the host's browser service)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-10-01 14:37:29 -04:00
parent 4878f1c5dd
commit 7d393add4a
18 changed files with 520 additions and 5 deletions

View File

@ -203,6 +203,10 @@ Generated-in-code art is original and has no file to list. This covers the defau
| `assets/icons/library.svg` | Lucide Icons and Contributors | https://lucide.dev (lucide-static v1.49.0; header kept in file) | ISC (`assets/icons/LICENSE-lucide.txt`) | |
| `assets/icons/link-2-off.svg` | Lucide Icons and Contributors | https://lucide.dev (lucide-static v1.49.0; header kept in file) | ISC (`assets/icons/LICENSE-lucide.txt`) | |
| `assets/icons/link.svg` | Lucide Icons and Contributors | https://lucide.dev (lucide-static v1.49.0; header kept in file) | ISC (`assets/icons/LICENSE-lucide.txt`) | |
| `assets/icons/external-link.svg` | Lucide Icons and Contributors | https://lucide.dev (lucide-static v1.49.0; header kept in file) | ISC (`assets/icons/LICENSE-lucide.txt`) | Help → app page link |
| `assets/icons/git-branch.svg` | Lucide Icons and Contributors | https://lucide.dev (lucide-static v1.49.0; header kept in file) | ISC (`assets/icons/LICENSE-lucide.txt`) | Help → GitHub link |
| `assets/icons/globe.svg` | Lucide Icons and Contributors | https://lucide.dev (lucide-static v1.49.0; header kept in file) | ISC (`assets/icons/LICENSE-lucide.txt`) | Help → website link |
| `assets/icons/message-circle.svg` | Lucide Icons and Contributors | https://lucide.dev (lucide-static v1.49.0; header kept in file) | ISC (`assets/icons/LICENSE-lucide.txt`) | Discord button and Help → Discord link |
| `assets/icons/lock-open.svg` | Lucide Icons and Contributors | https://lucide.dev (lucide-static v1.49.0; header kept in file) | ISC (`assets/icons/LICENSE-lucide.txt`) | |
| `assets/icons/lock.svg` | Lucide Icons and Contributors | https://lucide.dev (lucide-static v1.49.0; header kept in file) | ISC (`assets/icons/LICENSE-lucide.txt`) | |
| `assets/icons/map.svg` | Lucide Icons and Contributors | https://lucide.dev (lucide-static v1.49.0; header kept in file) | ISC (`assets/icons/LICENSE-lucide.txt`) | |

223
Cargo.lock generated
View File

@ -793,6 +793,17 @@ dependencies = [
"objc2 0.6.4",
]
[[package]]
name = "displaydoc"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.6",
]
[[package]]
name = "dlib"
version = "0.5.3"
@ -1234,6 +1245,15 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
[[package]]
name = "form_urlencoded"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
dependencies = [
"percent-encoding",
]
[[package]]
name = "futures-core"
version = "0.3.34"
@ -1621,6 +1641,110 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "icu_collections"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513"
dependencies = [
"displaydoc",
"potential_utf",
"utf8_iter",
"yoke",
"zerofrom",
"zerovec",
]
[[package]]
name = "icu_locale_core"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb"
dependencies = [
"displaydoc",
"litemap",
"tinystr",
"writeable",
"zerovec",
]
[[package]]
name = "icu_normalizer"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f"
dependencies = [
"icu_collections",
"icu_normalizer_data",
"icu_properties",
"icu_provider",
"smallvec",
"zerovec",
]
[[package]]
name = "icu_normalizer_data"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0"
[[package]]
name = "icu_properties"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148"
dependencies = [
"displaydoc",
"icu_collections",
"icu_locale_core",
"icu_properties_data",
"icu_provider",
"zerotrie",
"zerovec",
]
[[package]]
name = "icu_properties_data"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa"
[[package]]
name = "icu_provider"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73"
dependencies = [
"displaydoc",
"icu_locale_core",
"writeable",
"yoke",
"zerofrom",
"zerotrie",
"zerovec",
]
[[package]]
name = "idna"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
dependencies = [
"idna_adapter",
"smallvec",
"utf8_iter",
]
[[package]]
name = "idna_adapter"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
dependencies = [
"icu_normalizer",
"icu_properties",
]
[[package]]
name = "image"
version = "0.25.10"
@ -1916,6 +2040,12 @@ version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "litemap"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae"
[[package]]
name = "litrs"
version = "1.0.0"
@ -2705,6 +2835,15 @@ dependencies = [
"portable-atomic",
]
[[package]]
name = "potential_utf"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661"
dependencies = [
"zerovec",
]
[[package]]
name = "ppv-lite86"
version = "0.2.21"
@ -3612,6 +3751,16 @@ dependencies = [
"strict-num",
]
[[package]]
name = "tinystr"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643"
dependencies = [
"displaydoc",
"zerovec",
]
[[package]]
name = "tinyvec"
version = "1.13.3"
@ -3784,6 +3933,18 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
[[package]]
name = "url"
version = "2.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
"serde",
]
[[package]]
name = "usvg"
version = "0.45.1"
@ -3834,6 +3995,12 @@ dependencies = [
"xmlwriter",
]
[[package]]
name = "utf8_iter"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "uuid"
version = "1.26.1"
@ -3858,6 +4025,7 @@ dependencies = [
"serde_json",
"vectorcraft-engine",
"vectorcraft-ui-egui",
"webbrowser",
]
[[package]]
@ -4455,6 +4623,22 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "webbrowser"
version = "1.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62c35be770821a214dbc362fc26908c853e776c0004294d0b10b8a6bad582f94"
dependencies = [
"jni",
"log",
"ndk-context",
"objc2 0.6.4",
"objc2-app-kit 0.3.2",
"objc2-foundation 0.3.2",
"url",
"web-sys",
]
[[package]]
name = "weezl"
version = "0.1.12"
@ -5007,6 +5191,12 @@ dependencies = [
"read-fonts 0.39.2",
]
[[package]]
name = "writeable"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"
[[package]]
name = "x11-dl"
version = "2.21.0"
@ -5263,6 +5453,39 @@ dependencies = [
"synstructure",
]
[[package]]
name = "zerotrie"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f"
dependencies = [
"displaydoc",
"yoke",
"zerofrom",
]
[[package]]
name = "zerovec"
version = "0.11.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8"
dependencies = [
"yoke",
"zerofrom",
"zerovec-derive",
]
[[package]]
name = "zerovec-derive"
version = "0.11.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.6",
]
[[package]]
name = "zlib-rs"
version = "0.6.8"

View File

@ -55,6 +55,7 @@ eframe = { version = "0.36", default-features = false }
egui_kittest = "0.36"
rfd = "0.17"
arboard = { version = "3.6", default-features = false }
webbrowser = "1"
[workspace.lints.rust]
unsafe_code = "deny"

View File

@ -20,6 +20,7 @@ log = { workspace = true }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
rfd = { workspace = true }
arboard = { workspace = true }
webbrowser = { workspace = true }
[target.'cfg(target_os = "macos")'.dependencies]
muda = { version = "0.21", default-features = false }

View File

@ -108,6 +108,10 @@ fn services() -> Services {
write: Some(Box::new(|p: &str, b: &[u8]| std::fs::write(p, b).map_err(|e| e.to_string()))),
// Menu-bar Paste never sees egui's Paste event, so read the clipboard directly.
clipboard_read: Some(Box::new(|| arboard::Clipboard::new().ok()?.get_text().ok())),
// Help → Discord / website / GitHub, the Discord button, About and Home links.
open_url: Some(Box::new(|url: &str| {
let _ = webbrowser::open(url);
})),
..Default::default()
}
}

View File

@ -0,0 +1,17 @@
<!-- @license lucide-static v1.49.0 - ISC -->
<svg
class="lucide lucide-external-link"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M15 3h6v6" />
<path d="M10 14 21 3" />
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
</svg>

After

Width:  |  Height:  |  Size: 416 B

View File

@ -0,0 +1,17 @@
<!-- @license lucide-static v1.49.0 - ISC -->
<svg
class="lucide lucide-git-branch"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M15 6a9 9 0 0 0-9 9V3" />
<circle cx="18" cy="6" r="3" />
<circle cx="6" cy="18" r="3" />
</svg>

After

Width:  |  Height:  |  Size: 394 B

17
assets/icons/globe.svg Normal file
View File

@ -0,0 +1,17 @@
<!-- @license lucide-static v1.49.0 - ISC -->
<svg
class="lucide lucide-globe"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="10" />
<path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" />
<path d="M2 12h20" />
</svg>

After

Width:  |  Height:  |  Size: 407 B

View File

@ -0,0 +1,15 @@
<!-- @license lucide-static v1.49.0 - ISC -->
<svg
class="lucide lucide-message-circle"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719" />
</svg>

After

Width:  |  Height:  |  Size: 432 B

View File

@ -0,0 +1,51 @@
//! Help → community and project links (the ArtCraft Discord, website, this app's page and its
//! GitHub repository). The commands return the URL; the frontend opens it.
use serde_json::{Value, json};
use super::*;
/// The app's id on getartcraft.com and GitHub.
pub const APP_ID: &str = "vectorcraft";
pub const DISCORD_URL: &str = "https://discord.gg/artcraft";
pub const WEBSITE_URL: &str = "https://getartcraft.com";
/// This app's page on the website.
pub fn app_page_url() -> String {
format!("{WEBSITE_URL}/apps/{APP_ID}")
}
/// This app's source repository.
pub fn github_url() -> String {
format!("https://github.com/storytold/{APP_ID}")
}
pub fn specs() -> Vec<CommandSpec> {
vec![
cmd!(query "help.discord", "Join Our Discord", ["Help"], None, "{} → {url} the ArtCraft community Discord", always, |_, _| url(DISCORD_URL.into())),
cmd!(query "help.website", "ArtCraft Website", ["Help"], None, "{} → {url}", always, |_, _| url(WEBSITE_URL.into())),
cmd!(query "help.appPage", "VectorCraft on getartcraft.com", ["Help"], None, "{} → {url} this app's page", always, |_, _| url(app_page_url())),
cmd!(query "help.github", "VectorCraft on GitHub", ["Help"], None, "{} → {url} source code, issues and releases", always, |_, _| url(github_url())),
cmd!(query "help.links", "Links", [], None, "{} → {discord, website, appPage, github}", always, |_, _| {
Ok(json!({ "discord": DISCORD_URL, "website": WEBSITE_URL, "appPage": app_page_url(), "github": github_url() }))
}),
]
}
fn url(u: String) -> Result<Value> {
Ok(json!({ "url": u }))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn links() {
let mut s = Session::new();
assert_eq!(s.execute("help.discord", &json!({})).unwrap()["url"], "https://discord.gg/artcraft");
assert_eq!(s.execute("help.appPage", &json!({})).unwrap()["url"], "https://getartcraft.com/apps/vectorcraft");
assert_eq!(s.execute("help.github", &json!({})).unwrap()["url"], "https://github.com/storytold/vectorcraft");
assert_eq!(s.execute("help.links", &json!({})).unwrap()["website"], "https://getartcraft.com");
}
}

View File

@ -14,6 +14,7 @@ mod edit;
mod effectcmd;
mod file;
mod fonts;
pub mod help;
mod layer;
mod live;
pub(crate) mod menucmds;
@ -156,6 +157,7 @@ pub fn command_specs() -> &'static [CommandSpec] {
v.extend(textedit::specs());
v.extend(textstyles::specs());
v.extend(fonts::specs());
v.extend(help::specs());
v.extend(threads::specs());
v.extend(docmenu::specs());
v.extend(docinfo::specs());

View File

@ -851,6 +851,10 @@ fn home(app: &mut VectorcraftApp, ui: &mut Ui, rect: egui::Rect) {
}
}
});
ui.add_space(28.0);
ui.label(egui::RichText::new("Community").font(theme::semibold(14.0)).color(t.text));
ui.add_space(10.0);
crate::community::links(app, ui);
}
fn kurbo_flatten(p: &BezPath, tol: f64, f: &mut impl FnMut(PathEl)) {

View File

@ -30,7 +30,7 @@ pub fn app_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
menus::menu_bar(app, ui);
}
let full = ui.max_rect();
let right = egui::Rect::from_min_max(egui::pos2(full.right() - 340.0, full.top()), full.right_bottom());
let right = egui::Rect::from_min_max(egui::pos2(full.right() - 440.0, full.top()), full.right_bottom());
let mut rui = ui.new_child(egui::UiBuilder::new().max_rect(right).layout(egui::Layout::right_to_left(egui::Align::Center)));
let ui = &mut rui;
// Workspace switcher: shows the current workspace, opens Window → Workspace.
@ -52,6 +52,8 @@ pub fn app_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
app.ui.palette_open = true;
app.ui.palette_query.clear();
}
ui.add_space(10.0);
crate::community::discord_button(app, ui, false);
});
});
}

View File

@ -0,0 +1,87 @@
//! Community and project links: the Discord button (header, About, Home) and the link list.
//! Icons are Lucide's (ISC); Discord's own logo is a trademark, so the button uses a chat icon
//! on Discord's colour with the word "Discord".
use egui::{Color32, CornerRadius, Sense, Ui, vec2};
use crate::theme::{self, Tokens};
use crate::{VectorcraftApp, icons};
/// Discord's brand colour (a colour, not a logo).
const DISCORD: Color32 = Color32::from_rgb(0x58, 0x65, 0xF2);
/// A Discord pill button: compact for the header, large for About / Home.
pub fn discord_button(app: &mut VectorcraftApp, ui: &mut Ui, large: bool) -> egui::Response {
let (h, font, label) = if large { (36.0, theme::semibold(14.0), "Join our Discord") } else { (24.0, theme::semibold(12.0), "Discord") };
let galley = ui.painter().layout_no_wrap(label.to_string(), font, Color32::WHITE);
let icon = h * 0.55;
let w = galley.size().x + icon + h * 0.9;
let (r, resp) = ui.allocate_exact_size(vec2(w, h), Sense::click());
let fill = if resp.hovered() { DISCORD.gamma_multiply(0.85) } else { DISCORD };
ui.painter().rect_filled(r, CornerRadius::same((h / 2.0) as u8), fill);
let ir = egui::Rect::from_center_size(r.left_center() + vec2(h * 0.35 + icon / 2.0, 0.0), vec2(icon, icon));
icons::paint(ui, "message-circle", ir, Color32::WHITE);
ui.painter().galley(egui::pos2(ir.right() + h * 0.2, r.center().y - galley.size().y / 2.0), galley, Color32::WHITE);
let resp = resp.on_hover_text("Join the ArtCraft community on Discord (discord.gg/artcraft)");
if resp.clicked() {
app.open_link("help.discord");
}
resp
}
/// One link row: icon, label, opens the command's URL.
fn link(app: &mut VectorcraftApp, ui: &mut Ui, icon: &str, label: &str, cmd: &str, url: &str) {
let t = Tokens::get(ui.ctx());
ui.horizontal(|ui| {
icons::icon(ui, icon, 16.0, t.icon);
let r = ui.add(egui::Button::new(egui::RichText::new(label).color(t.accent).size(13.0)).frame(false)).on_hover_text(url);
if r.clicked() {
app.open_link(cmd);
}
});
}
/// The Discord button and the website / app page / GitHub links.
pub fn links(app: &mut VectorcraftApp, ui: &mut Ui) {
discord_button(app, ui, true);
ui.add_space(8.0);
let l = app.session.execute("help.links", &serde_json::json!({})).unwrap_or_default();
let s = |k: &str| l[k].as_str().unwrap_or("").to_string();
link(app, ui, "globe", "ArtCraft website", "help.website", &s("website"));
link(app, ui, "external-link", "VectorCraft on getartcraft.com", "help.appPage", &s("appPage"));
link(app, ui, "git-branch", "Source code on GitHub", "help.github", &s("github"));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn link_clicks_open_the_right_urls() {
use std::sync::{Arc, Mutex};
let opened = Arc::new(Mutex::new(vec![]));
let o = opened.clone();
let services = crate::Services { open_url: Some(Box::new(move |u: &str| o.lock().unwrap().push(u.to_string()))), ..Default::default() };
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), services);
for id in ["help.discord", "help.website", "help.appPage", "help.github"] {
crate::menus::invoke(&mut app, id, serde_json::json!({}));
}
assert_eq!(
*opened.lock().unwrap(),
[
"https://discord.gg/artcraft",
"https://getartcraft.com",
"https://getartcraft.com/apps/vectorcraft",
"https://github.com/storytold/vectorcraft"
]
);
// The control channel / MCP path returns the URL without opening a browser.
let v = app.run("help.discord", serde_json::json!({})).unwrap();
assert_eq!(v["url"], "https://discord.gg/artcraft");
assert_eq!(opened.lock().unwrap().len(), 4);
let ctx = egui::Context::default();
theme::install_fonts(&ctx);
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| links(&mut app, ui));
out.textures_delta.clear();
}
}

View File

@ -251,10 +251,26 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) {
.resizable(false)
.open(&mut open)
.anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0])
.frame(egui::Frame::window(&ctx.global_style()).inner_margin(egui::Margin::same(18)))
.show(ctx, |ui| {
ui.label(egui::RichText::new("VectorCraft").font(theme::semibold(22.0)));
ui.label(format!("Version {} — open-source vector illustration in pure Rust.", env!("CARGO_PKG_VERSION")));
ui.label("MIT OR Apache-2.0. Fonts: Source Sans 3, Inter, JetBrains Mono (OFL). Icons: Lucide (ISC) + VectorCraft.");
ui.set_width(380.0);
ui.horizontal(|ui| {
let (r, _) = ui.allocate_exact_size(egui::vec2(44.0, 44.0), egui::Sense::hover());
crate::brand::paint_mark(ui.painter(), r);
ui.vertical(|ui| {
ui.label(egui::RichText::new("VectorCraft").font(theme::semibold(22.0)));
ui.label(format!("Version {} — open-source vector illustration in pure Rust.", env!("CARGO_PKG_VERSION")));
});
});
ui.add_space(12.0);
crate::community::links(app, ui);
ui.add_space(12.0);
ui.label(
egui::RichText::new(
"Part of ArtCraft. MIT OR Apache-2.0. Fonts: Source Sans 3, Inter, JetBrains Mono (OFL). Icons: Lucide (ISC) + VectorCraft.",
)
.size(11.0),
);
});
app.ui.about = open;
}

View File

@ -182,6 +182,10 @@ pub static ICONS: &[(&str, &[u8])] = &[
("layout-grid", include_bytes!("../../../assets/icons/layout-grid.svg")),
("library", include_bytes!("../../../assets/icons/library.svg")),
("link", include_bytes!("../../../assets/icons/link.svg")),
("external-link", include_bytes!("../../../assets/icons/external-link.svg")),
("git-branch", include_bytes!("../../../assets/icons/git-branch.svg")),
("globe", include_bytes!("../../../assets/icons/globe.svg")),
("message-circle", include_bytes!("../../../assets/icons/message-circle.svg")),
("link-2-off", include_bytes!("../../../assets/icons/link-2-off.svg")),
("lock", include_bytes!("../../../assets/icons/lock.svg")),
("lock-open", include_bytes!("../../../assets/icons/lock-open.svg")),

View File

@ -8,6 +8,7 @@
mod brand;
pub mod canvas;
pub mod chrome;
pub mod community;
pub mod control;
pub mod cursors;
pub mod dialogs;
@ -44,6 +45,9 @@ pub type WriteFn = Box<dyn FnMut(&str, &[u8]) -> Result<(), String>>;
pub type Inbox = Arc<Mutex<Vec<(String, Vec<u8>)>>>;
pub type DownloadFn = Box<dyn FnMut(&str, &[u8])>;
/// Opens a URL in the system browser.
pub type OpenUrlFn = Box<dyn FnMut(&str)>;
/// Platform services injected by the host app (desktop or web).
#[derive(Default)]
pub struct Services {
@ -62,6 +66,8 @@ pub struct Services {
/// Read the system clipboard's text (desktop). Without it, pasted text only arrives with
/// egui's Paste event (web, and keyboard paste everywhere).
pub clipboard_read: Option<Box<dyn FnMut() -> Option<String>>>,
/// Open a URL in the system browser (desktop). Without it, egui opens it (a new tab on the web).
pub open_url: Option<OpenUrlFn>,
}
/// Cached canvas raster.
@ -131,6 +137,8 @@ pub struct VectorcraftApp {
clipboard_out: Option<String>,
clipboard_published: Option<String>,
pub(crate) clipboard_in: Option<String>,
/// A URL to open through egui next frame (when the host has no `open_url` service).
pending_url: Option<String>,
}
impl VectorcraftApp {
@ -157,6 +165,7 @@ impl VectorcraftApp {
clipboard_out: None,
clipboard_published: None,
clipboard_in: None,
pending_url: None,
control_rx: None,
pending_screenshots: vec![],
queued_screenshots: vec![],
@ -244,6 +253,25 @@ impl VectorcraftApp {
}
}
/// Open a link in the browser (Help → Discord, website, GitHub…).
pub fn open_url(&mut self, url: &str) {
match self.services.open_url.as_mut() {
Some(open) => open(url),
None => self.pending_url = Some(url.to_string()),
}
self.ui.status = format!("Opened {url}");
}
/// Run a Help link command and open the URL it returns.
pub fn open_link(&mut self, id: &str) {
if let Ok(v) = self.run(id, serde_json::json!({}))
&& let Some(u) = v["url"].as_str()
{
let u = u.to_string();
self.open_url(&u);
}
}
/// Select a tool (also used by the toolbar and shortcuts).
pub fn select_tool(&mut self, id: &str) {
let v = self.view_info();
@ -388,6 +416,9 @@ impl VectorcraftApp {
}
self.collect_screenshots(ctx);
self.issue_screenshots(ctx);
if let Some(u) = self.pending_url.take() {
ctx.open_url(egui::OpenUrl::new_tab(u));
}
if let Some(t) = self.clipboard_out.take() {
ctx.copy_text(t);
}

View File

@ -512,6 +512,7 @@ pub fn menu_tree() -> Vec<(&'static str, Vec<Item>)> {
"VectorCraft",
vec![
c("About VectorCraft", "help.about"),
c("Join Our Discord", "help.discord"),
Sep,
c("Settings…", "edit.preferences"),
Sep,
@ -1058,7 +1059,20 @@ pub fn menu_tree() -> Vec<(&'static str, Vec<Item>)> {
sub("Symbol Libraries", vec![todo("Arrows"), todo("Charts"), todo("Web Buttons and Bars")]),
],
),
("Help", vec![c("Search Commands…", "help.commandPalette"), todos("VectorCraft Help…", "F1"), Sep, c("About VectorCraft", "help.about")]),
(
"Help",
vec![
c("Join Our Discord", "help.discord"),
c("ArtCraft Website", "help.website"),
c("VectorCraft on getartcraft.com", "help.appPage"),
c("VectorCraft on GitHub", "help.github"),
Sep,
c("Search Commands…", "help.commandPalette"),
todos("VectorCraft Help…", "F1"),
Sep,
c("About VectorCraft", "help.about"),
],
),
]
}
@ -1186,6 +1200,11 @@ pub fn invoke(app: &mut VectorcraftApp, id: &str, p: Value) {
}
return;
}
// Help links: the command returns the URL; a menu click opens it (agents just get the URL).
if matches!(id, "help.discord" | "help.website" | "help.appPage" | "help.github") {
app.open_link(id);
return;
}
if let Err(e) = app.run(id, p) {
app.status(e);
} else if matches!(id, "object.pattern.make" | "object.pattern.edit") {