mirror of
https://github.com/storytold/craft-launcher.git
synced 2026-10-14 14:26:48 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f295d4b9c | ||
|
|
1579aa8c02 |
No files matched your search
@@ -161,12 +161,19 @@ fn native_options(hidden: bool) -> eframe::NativeOptions {
|
||||
/// Adrenalin installer leaves the integrated GPU on the OEM driver) it crashes inside
|
||||
/// `amdvlk64.dll` before the first frame. `WGPU_BACKEND` still overrides the default, and then
|
||||
/// wgpu picks the adapter itself. Direct3D 12 shaders compile with FXC ([`pin_dx12_compiler`]).
|
||||
///
|
||||
/// Direct3D 12 presents through DirectComposition rather than straight to the window. The
|
||||
/// borderless window's drop shadow moves its client area down a pixel, and a swapchain made from
|
||||
/// the window is then stretched over it vertically by the compositor, blurring every horizontal
|
||||
/// edge and line of text. `WGPU_DX12_PRESENTATION_SYSTEM` still overrides it.
|
||||
#[cfg(windows)]
|
||||
fn wgpu_options() -> eframe::WgpuConfiguration {
|
||||
use eframe::wgpu::{Backends, Dx12Compiler, PowerPreference};
|
||||
use eframe::wgpu::{Backends, Dx12Compiler, Dx12SwapchainKind, PowerPreference};
|
||||
let mut config = eframe::WgpuConfiguration::default();
|
||||
pin_dx12_compiler(&mut config, Dx12Compiler::from_env());
|
||||
if let eframe::egui_wgpu::WgpuSetup::CreateNew(setup) = &mut config.wgpu_setup {
|
||||
setup.instance_descriptor.backend_options.dx12.presentation_system =
|
||||
Dx12SwapchainKind::from_env().unwrap_or(Dx12SwapchainKind::DxgiFromVisual);
|
||||
if let Some(backends) = Backends::from_env() {
|
||||
setup.instance_descriptor.backends = backends;
|
||||
} else {
|
||||
|
||||
@@ -22,6 +22,7 @@ serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
log = { workspace = true }
|
||||
dirs = "6"
|
||||
tempfile = "3"
|
||||
sha2 = "0.10"
|
||||
# HTTPS over rustls, with the OS verifying certificates (rustls-platform-verifier). No bundled
|
||||
# root list: webpki-roots and webpki-root-certs are CDLA-licensed.
|
||||
@@ -36,7 +37,3 @@ plist = "1"
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
winreg = "0.55"
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
+104
-14
@@ -354,23 +354,36 @@ fn prepare_home(home: &Path) {
|
||||
/// Moves `staged` into place at `dest`, keeping the old copy until the new one is there.
|
||||
#[allow(dead_code)]
|
||||
fn swap_into_place(staged: &Path, dest: &Path) -> Result<(), String> {
|
||||
let old = dest.with_extension("old");
|
||||
if old.exists() {
|
||||
let _ = if old.is_dir() { std::fs::remove_dir_all(&old) } else { std::fs::remove_file(&old) };
|
||||
}
|
||||
let had_old = dest.exists();
|
||||
if had_old {
|
||||
std::fs::rename(dest, &old).map_err(|e| format!("couldn't move the old copy aside ({e})"))?;
|
||||
}
|
||||
if let Err(e) = std::fs::rename(staged, dest) {
|
||||
if had_old {
|
||||
let _ = std::fs::rename(&old, dest);
|
||||
swap_into_place_via(staged, dest, |from, to| std::fs::rename(from, to))
|
||||
}
|
||||
|
||||
fn swap_into_place_via(staged: &Path, dest: &Path, rename: impl Fn(&Path, &Path) -> std::io::Result<()>) -> Result<(), String> {
|
||||
// Reserve a directory of our own beside the destination. A predictable sibling such as
|
||||
// `app.old` may be a user's backup, and must never be removed or overwritten.
|
||||
let backup = if dest.exists() {
|
||||
let parent = dest.parent().ok_or("the installed app has no parent folder")?;
|
||||
let backup = tempfile::Builder::new()
|
||||
.prefix(".artcraft-launcher-backup-")
|
||||
.tempdir_in(parent)
|
||||
.map_err(|e| format!("couldn't create a backup folder in {} ({e})", parent.display()))?;
|
||||
rename(dest, &backup.path().join("old")).map_err(|e| format!("couldn't move the old copy aside ({e})"))?;
|
||||
Some(backup)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Err(e) = rename(staged, dest) {
|
||||
if let Some(backup) = backup
|
||||
&& let Err(restore) = rename(&backup.path().join("old"), dest)
|
||||
{
|
||||
// A failed rollback must not let TempDir's cleanup delete the only old copy.
|
||||
let saved = backup.keep().join("old");
|
||||
return Err(format!(
|
||||
"couldn't put the new copy in place ({e}); couldn't restore the old copy ({restore}); it was kept at {}",
|
||||
saved.display()
|
||||
));
|
||||
}
|
||||
return Err(format!("couldn't put the new copy in place ({e})"));
|
||||
}
|
||||
if had_old {
|
||||
let _ = if old.is_dir() { std::fs::remove_dir_all(&old) } else { std::fs::remove_file(&old) };
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1229,9 +1242,86 @@ mod tests {
|
||||
swap_into_place(&staged, &dest).unwrap();
|
||||
assert_eq!(std::fs::read_to_string(dest.join("v")).unwrap(), "new");
|
||||
assert!(!staged.exists() && !dir.path().join("app.old").exists());
|
||||
assert_eq!(std::fs::read_dir(dir.path()).unwrap().count(), 1);
|
||||
// A missing staged copy leaves the installed one alone.
|
||||
assert!(swap_into_place(&staged, &dest).is_err());
|
||||
assert_eq!(std::fs::read_to_string(dest.join("v")).unwrap(), "new");
|
||||
assert_eq!(std::fs::read_dir(dir.path()).unwrap().count(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swap_preserves_an_unrelated_backup_file() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let dest = dir.path().join("app");
|
||||
let staged = dir.path().join("app.new");
|
||||
let unrelated = dest.with_extension("old");
|
||||
std::fs::write(&dest, b"installed app").unwrap();
|
||||
std::fs::write(&staged, b"updated app").unwrap();
|
||||
std::fs::write(&unrelated, b"user backup").unwrap();
|
||||
swap_into_place(&staged, &dest).unwrap();
|
||||
assert_eq!(std::fs::read(&unrelated).unwrap(), b"user backup");
|
||||
assert_eq!(std::fs::read(&dest).unwrap(), b"updated app");
|
||||
assert_eq!(std::fs::read_dir(dir.path()).unwrap().count(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swap_preserves_an_unrelated_backup_directory() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let dest = dir.path().join("app");
|
||||
let staged = dir.path().join("app.new");
|
||||
let unrelated = dest.with_extension("old");
|
||||
std::fs::create_dir(&dest).unwrap();
|
||||
std::fs::write(dest.join("v"), b"installed app").unwrap();
|
||||
std::fs::create_dir(&staged).unwrap();
|
||||
std::fs::write(staged.join("v"), b"updated app").unwrap();
|
||||
std::fs::create_dir(&unrelated).unwrap();
|
||||
std::fs::write(unrelated.join("keep.txt"), b"user backup").unwrap();
|
||||
swap_into_place(&staged, &dest).unwrap();
|
||||
assert_eq!(std::fs::read(unrelated.join("keep.txt")).unwrap(), b"user backup");
|
||||
assert_eq!(std::fs::read(dest.join("v")).unwrap(), b"updated app");
|
||||
assert_eq!(std::fs::read_dir(dir.path()).unwrap().count(), 2);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn swap_preserves_a_dangling_backup_symlink() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let dest = dir.path().join("app");
|
||||
let staged = dir.path().join("app.new");
|
||||
let unrelated = dest.with_extension("old");
|
||||
let target = dir.path().join("missing");
|
||||
std::fs::write(&dest, b"installed app").unwrap();
|
||||
std::fs::write(&staged, b"updated app").unwrap();
|
||||
std::os::unix::fs::symlink(&target, &unrelated).unwrap();
|
||||
swap_into_place(&staged, &dest).unwrap();
|
||||
assert_eq!(std::fs::read_link(&unrelated).unwrap(), target);
|
||||
assert_eq!(std::fs::read(&dest).unwrap(), b"updated app");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_failed_rollback_keeps_the_old_app_and_reports_its_backup_path() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let dest = dir.path().join("app");
|
||||
let staged = dir.path().join("app.new");
|
||||
std::fs::write(&dest, b"installed app").unwrap();
|
||||
std::fs::write(&staged, b"updated app").unwrap();
|
||||
let calls = std::cell::Cell::new(0);
|
||||
let err = swap_into_place_via(&staged, &dest, |from, to| {
|
||||
calls.set(calls.get() + 1);
|
||||
match calls.get() {
|
||||
1 => std::fs::rename(from, to),
|
||||
2 => Err(std::io::Error::other("staged move failed")),
|
||||
_ => Err(std::io::Error::other("restore failed")),
|
||||
}
|
||||
})
|
||||
.unwrap_err();
|
||||
assert_eq!(calls.get(), 3);
|
||||
assert!(err.contains("staged move failed") && err.contains("restore failed"));
|
||||
let backup = std::fs::read_dir(dir.path()).unwrap().map(|e| e.unwrap().path()).find(|p| p.is_dir()).unwrap();
|
||||
let saved = backup.join("old");
|
||||
assert_eq!(std::fs::read(&saved).unwrap(), b"installed app");
|
||||
assert!(err.contains(&saved.display().to_string()));
|
||||
assert_eq!(std::fs::read(&staged).unwrap(), b"updated app");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -287,7 +287,9 @@ pub fn install_fonts(ctx: &egui::Context, cjk: Option<Lang>) {
|
||||
("GeistMono-Bold", GEIST_MONO, &[(*b"wght", 700.0)], mono_bold()),
|
||||
];
|
||||
for (name, bytes, coords, family) in faces {
|
||||
let tweak = FontTweak { coords: VariationCoords::new(coords.iter().copied()), ..Default::default() };
|
||||
// No subpixel binning: these fonts carry no hinting instructions, so a glyph drawn at a
|
||||
// fractional x smears each stem across two pixels, leaving small text blurry at 100 %.
|
||||
let tweak = FontTweak { coords: VariationCoords::new(coords.iter().copied()), subpixel_binning: Some(false), ..Default::default() };
|
||||
fonts.font_data.insert(name.to_owned(), Arc::new(FontData::from_static(bytes).tweak(tweak)));
|
||||
let mut stack = vec![name.to_owned()];
|
||||
stack.extend(fallback.iter().cloned());
|
||||
|
||||
@@ -411,9 +411,11 @@ impl LauncherUi {
|
||||
// A faint wash in the app's colour fades in under the pointer.
|
||||
let fill = t.bg.lerp_to_gamma(app_color, 0.05 * lift);
|
||||
painter.rect(rect, CornerRadius::ZERO, fill, t.hairline(), egui::StrokeKind::Inside);
|
||||
// On hover, a 2 px rule in the app's colour wipes in from the left along the top.
|
||||
// On hover, a 2 px rule in the app's colour wipes in from the left along the top, inside the
|
||||
// hairlines the card shares with its neighbours (the site's rule sits within its grid gaps).
|
||||
if lift > 0.0 {
|
||||
painter.rect_filled(Rect::from_min_size(rect.min, vec2(rect.width() * lift, 2.0)), CornerRadius::ZERO, app_color);
|
||||
let inside = rect.shrink(1.0);
|
||||
painter.rect_filled(Rect::from_min_size(inside.min, vec2(inside.width() * lift, 2.0)), CornerRadius::ZERO, app_color);
|
||||
}
|
||||
|
||||
// The top strip: the app's number in its colour, and its pages.
|
||||
|
||||
Reference in new issue
Block a user