ctx.open_url is unreliable on native (notably Windows), so the Help menu, the Discord buttons, and the start-page links did nothing. Added an open_url Service hook wired to the `open` crate on native (ShellExecute on Windows); links::open prefers it and falls back to ctx.open_url (web). link_row now routes clicks through it instead of ui.hyperlink_to. Test drives every Help link id + the open() helper through a mock service. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
b7cac05f69
commit
34d138c11f
|
|
@ -3241,6 +3241,7 @@ dependencies = [
|
|||
"eframe",
|
||||
"egui",
|
||||
"log",
|
||||
"open",
|
||||
"photocraft-codecs",
|
||||
"photocraft-color",
|
||||
"photocraft-compose",
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ log = { workspace = true }
|
|||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
rfd = { workspace = true }
|
||||
arboard = "3"
|
||||
open = "5"
|
||||
|
||||
[build-dependencies]
|
||||
# Icon + VERSIONINFO for photocraft.exe; build.rs only uses it when targeting Windows.
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ pub fn native() -> Services {
|
|||
photocraft_codecs::encode(&img, photocraft_codecs::Format::Png, &EncodeOptions::default()).map_err(|e| e.to_string())
|
||||
})),
|
||||
inbox: None,
|
||||
open_url: Some(Box::new(|url: &str| open::that(url).map_err(|e| e.to_string()))),
|
||||
clipboard_set_image: Some(Box::new(|w: u32, h: u32, px: &[u8]| {
|
||||
let mut cb = arboard::Clipboard::new().map_err(|e| e.to_string())?;
|
||||
cb.set_image(arboard::ImageData { width: w as usize, height: h as usize, bytes: std::borrow::Cow::Borrowed(px) }).map_err(|e| e.to_string())
|
||||
|
|
|
|||
|
|
@ -597,7 +597,7 @@ fn start_screen(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
|
|||
ui.add_space(26.0);
|
||||
crate::links::discord_button(app, ui, 190.0);
|
||||
ui.add_space(10.0);
|
||||
crate::links::link_row(ui);
|
||||
crate::links::link_row(app, ui);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ pub fn show(app: &mut PhotocraftApp, ctx: &egui::Context) {
|
|||
ui.vertical_centered(|ui| {
|
||||
crate::links::discord_button(app, ui, 220.0);
|
||||
ui.add_space(8.0);
|
||||
crate::links::link_row(ui);
|
||||
crate::links::link_row(app, ui);
|
||||
});
|
||||
ui.add_space(10.0);
|
||||
ui.weak("egui · wgpu · photocraft-engine");
|
||||
|
|
|
|||
|
|
@ -90,6 +90,8 @@ pub type ExportFn = Box<dyn Fn(&Document, &str, &ExportSettings) -> Result<Vec<u
|
|||
pub type PickOpenFn = Box<dyn FnMut() -> Option<(String, Vec<u8>)>>;
|
||||
pub type PickSaveFn = Box<dyn FnMut(&str) -> Option<String>>;
|
||||
pub type WriteFn = Box<dyn FnMut(&str, &[u8]) -> Result<(), String>>;
|
||||
/// Open a URL in the system browser (native) — reliable cross-platform, unlike `ctx.open_url`.
|
||||
pub type OpenUrlFn = Box<dyn Fn(&str) -> Result<(), String>>;
|
||||
pub type EncodePngFn = Box<dyn Fn(u32, u32, &[u8]) -> Result<Vec<u8>, String>>;
|
||||
/// Put an RGBA8 image (width, height, pixels) on the OS clipboard.
|
||||
pub type ClipboardSetFn = Box<dyn FnMut(u32, u32, &[u8]) -> Result<(), String>>;
|
||||
|
|
@ -126,6 +128,8 @@ pub struct Services {
|
|||
pub write: Option<WriteFn>,
|
||||
/// Encode an RGBA8 image as PNG (used for screenshots and `ui.render`).
|
||||
pub encode_png: Option<EncodePngFn>,
|
||||
/// Open a URL in the system browser (native). Falls back to `ctx.open_url` (web) when unset.
|
||||
pub open_url: Option<OpenUrlFn>,
|
||||
/// Files delivered asynchronously (web file pickers, drag-and-drop): drained every frame.
|
||||
pub inbox: Option<Inbox>,
|
||||
/// OS clipboard images: copies go out, screenshots and images from other apps come in.
|
||||
|
|
|
|||
|
|
@ -25,9 +25,13 @@ pub fn url_for(id: &str) -> Option<&'static str> {
|
|||
COMMANDS.iter().find(|c| c.0 == id).map(|c| c.1)
|
||||
}
|
||||
|
||||
/// Open `url` in the system browser (a new tab on the web) and note it in the status bar.
|
||||
/// Open `url` in the system browser (a new tab on the web) and note it in the status bar. Prefers
|
||||
/// the platform `open_url` service (reliable on Windows/macOS/Linux); falls back to `ctx.open_url`.
|
||||
pub fn open(app: &mut PhotocraftApp, ctx: &egui::Context, url: &str) -> Value {
|
||||
ctx.open_url(egui::OpenUrl::new_tab(url));
|
||||
let opened = app.services.open_url.as_ref().map(|f| f(url).is_ok()).unwrap_or(false);
|
||||
if !opened {
|
||||
ctx.open_url(egui::OpenUrl::new_tab(url));
|
||||
}
|
||||
app.ui.status = format!("Opened {url}");
|
||||
json!({"url": url})
|
||||
}
|
||||
|
|
@ -41,14 +45,16 @@ pub fn discord_button(app: &mut PhotocraftApp, ui: &mut egui::Ui, min_width: f32
|
|||
r
|
||||
}
|
||||
|
||||
/// "PhotoCraft website · GitHub · ArtCraft" as hyperlinks, centred in the available width.
|
||||
pub fn link_row(ui: &mut egui::Ui) {
|
||||
/// "PhotoCraft website · GitHub · ArtCraft" as links, centred. Clicks route through [`open`] (the
|
||||
/// platform browser service) rather than `ui.hyperlink_to`, which uses the unreliable `ctx.open_url`.
|
||||
pub fn link_row(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
|
||||
let t = crate::theme::Tokens::get(ui.ctx());
|
||||
let links = [("PhotoCraft website", APP_PAGE), ("GitHub", GITHUB), ("ArtCraft", ARTCRAFT_WEBSITE)];
|
||||
let font = egui::FontId::proportional(12.5);
|
||||
let sep = " · ";
|
||||
let width: f32 = links.iter().map(|(l, _)| ui.painter().layout_no_wrap((*l).into(), font.clone(), t.text).size().x).sum::<f32>()
|
||||
+ 2.0 * ui.painter().layout_no_wrap(sep.into(), font.clone(), t.text).size().x;
|
||||
let mut clicked: Option<&str> = None;
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
ui.add_space(((ui.available_width() - width) / 2.0).max(0.0));
|
||||
|
|
@ -56,15 +62,46 @@ pub fn link_row(ui: &mut egui::Ui) {
|
|||
if i > 0 {
|
||||
ui.label(egui::RichText::new(sep).font(font.clone()).color(t.text_faint));
|
||||
}
|
||||
ui.hyperlink_to(egui::RichText::new(*label).font(font.clone()), *url).on_hover_text(*url);
|
||||
if ui.link(egui::RichText::new(*label).font(font.clone()).color(t.accent)).on_hover_text(*url).clicked() {
|
||||
clicked = Some(url);
|
||||
}
|
||||
}
|
||||
});
|
||||
if let Some(url) = clicked {
|
||||
open(app, ui.ctx(), url);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn links_open_through_the_platform_service() {
|
||||
// Issue #14: links (Help menu, Discord button, start-page links) must open in the browser.
|
||||
// They route through the `open_url` service rather than the unreliable `ctx.open_url`.
|
||||
use std::sync::{Arc, Mutex};
|
||||
let opened: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
let rec = opened.clone();
|
||||
let services = crate::Services {
|
||||
open_url: Some(Box::new(move |u: &str| {
|
||||
rec.lock().unwrap().push(u.to_string());
|
||||
Ok(())
|
||||
})),
|
||||
..Default::default()
|
||||
};
|
||||
let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), services);
|
||||
let ctx = egui::Context::default();
|
||||
// Every Help-menu link id reaches the service with its URL.
|
||||
for (id, url) in COMMANDS {
|
||||
crate::menus::invoke(&mut app, &ctx, id, json!({})).unwrap();
|
||||
assert_eq!(opened.lock().unwrap().last().map(String::as_str), Some(*url), "{id}");
|
||||
}
|
||||
// The direct open() helper (Discord button / start-page links) also uses it.
|
||||
open(&mut app, &ctx, DISCORD);
|
||||
assert_eq!(opened.lock().unwrap().last().map(String::as_str), Some(DISCORD));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_menu_lists_links_then_separator_then_about() {
|
||||
let app = PhotocraftApp::new(photocraft_engine::Session::new(), Default::default());
|
||||
|
|
|
|||
Loading…
Reference in New Issue