Give Horizon a mark, and its icon everywhere
The mark is an H whose crossbar is the horizon: a violet line edge to edge, the stems above it and their reflection below at a third, on whole units of a 16-unit grid so it stays sharp at 16 px. docs/media/brand holds the SVG masters (square icon, macOS tile, avatars, the mark for light and dark backgrounds), the two alternatives that were shown with it, the GitHub social preview and the Discord banner, and a README of what goes where and how to upload what only Ludovic's accounts can. `cargo run -p xtask -- icons` renders the masters (rsvg-convert) and packs, in plain Rust: packaging/icons/horizon.ico and horizon.icns, Linux's hicolor icons 16–512 and the scalable SVG, the game's run-time window icon, the avatars and the website's favicons. packaging/linux has .desktop entries for the launcher and the game. On Windows, build.rs embeds the .ico as icon resource 1 in horizon-client (and its tools) and horizon-launcher (winresource, a Windows-only build dependency); gpui loads that resource as the launcher's window icon. The game window sets the icon itself (X11, Windows) and names itself `horizon` on Wayland so the desktop entry matches. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@@ -923,6 +923,7 @@ dependencies = [
|
||||
"ureq",
|
||||
"wgpu",
|
||||
"winit",
|
||||
"winresource",
|
||||
"zip",
|
||||
]
|
||||
|
||||
@@ -2347,6 +2348,15 @@ dependencies = [
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e7523beb55eece201a2356bee0bbca0d1ab466c14c07703b2e0ee6d42cb0c2c"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1"
|
||||
version = "0.10.7"
|
||||
@@ -2664,6 +2674,21 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "1.1.6+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "920602543f0911ab71da12c50d59701da54c196d1a2bf5cb4b75667f137a406a"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde_core",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_parser",
|
||||
"toml_writer",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "1.1.1+spec-1.1.0"
|
||||
@@ -2694,6 +2719,12 @@ dependencies = [
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_writer"
|
||||
version = "1.1.3+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06bdbd8cfc056b8d2e2e85f29b56a3bdbecb527cef81eb39e3e7b98af4652770"
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.44"
|
||||
@@ -3559,6 +3590,16 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winresource"
|
||||
version = "0.1.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2bc6d36480b829272a5bdabf08e6dca8004761c1da7e35efef800540c66221b"
|
||||
dependencies = [
|
||||
"toml",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.57.1"
|
||||
@@ -3649,6 +3690,9 @@ checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"horizon-theme",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xxhash-rust"
|
||||
|
||||
@@ -34,6 +34,10 @@ gilrs = "0.11"
|
||||
# HTTPS to the Minecraft services (online-mode session join).
|
||||
ureq = { version = "2", default-features = false, features = ["tls"] }
|
||||
|
||||
[target.'cfg(windows)'.build-dependencies]
|
||||
# The icon resource in the .exe (build.rs).
|
||||
winresource = "0.1"
|
||||
|
||||
[features]
|
||||
# Records the call sites of mob random draws (`mob_ai_probe` traces).
|
||||
rng-trace = ["horizon-core/rng-trace"]
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
//! Embeds Horizon's icon (`packaging/icons/horizon.ico`) in the Windows executables, as icon
|
||||
//! resource 1: Explorer and the task bar show it, and the window takes it as its icon.
|
||||
fn main() {
|
||||
let dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_default();
|
||||
let ico = format!("{dir}/../../packaging/icons/horizon.ico");
|
||||
println!("cargo:rerun-if-changed={ico}");
|
||||
#[cfg(windows)]
|
||||
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows") {
|
||||
let mut resource = winresource::WindowsResource::new();
|
||||
resource.set_icon(&ico);
|
||||
if let Err(e) = resource.compile() {
|
||||
panic!("embedding {ico}: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8199,9 +8199,16 @@ impl ApplicationHandler for Game<'_> {
|
||||
// The Window constructor: created fullscreen when options.txt says so.
|
||||
let fullscreen = self.settings.fullscreen;
|
||||
self.fullscreen = horizon_client::window::FullscreenState::new(fullscreen);
|
||||
let attributes = Window::default_attributes()
|
||||
.with_title("Horizon")
|
||||
.with_window_icon(horizon_client::window::icon());
|
||||
// Wayland names the window by its app id, which the horizon.desktop entry matches.
|
||||
#[cfg(target_os = "linux")]
|
||||
let attributes = winit::platform::wayland::WindowAttributesExtWayland::with_name(
|
||||
attributes, "horizon", "horizon",
|
||||
);
|
||||
let window = match event_loop.create_window(
|
||||
Window::default_attributes()
|
||||
.with_title("Horizon")
|
||||
attributes
|
||||
.with_inner_size(winit::dpi::LogicalSize::new(1280.0, 720.0))
|
||||
.with_fullscreen(fullscreen.then_some(winit::window::Fullscreen::Borderless(None))),
|
||||
) {
|
||||
|
||||
@@ -307,10 +307,30 @@ impl FullscreenState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Horizon's icon for the window's title bar and the task bar (X11 and Windows; Wayland takes
|
||||
/// it from the `horizon.desktop` entry, macOS from the app bundle). `None` if it can't be read.
|
||||
pub fn icon() -> Option<winit::window::Icon> {
|
||||
let mut decoder = png::Decoder::new(std::io::Cursor::new(horizon_theme::ICON_PNG));
|
||||
decoder.set_transformations(png::Transformations::EXPAND | png::Transformations::STRIP_16);
|
||||
let mut reader = decoder.read_info().ok()?;
|
||||
let mut bytes = vec![0; reader.output_buffer_size()?];
|
||||
let info = reader.next_frame(&mut bytes).ok()?;
|
||||
if info.color_type != png::ColorType::Rgba {
|
||||
return None;
|
||||
}
|
||||
bytes.truncate(info.buffer_size());
|
||||
winit::window::Icon::from_rgba(bytes, info.width, info.height).ok()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn the_window_icon_decodes() {
|
||||
assert!(icon().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fullscreen_follows_the_option_only_when_it_changes() {
|
||||
let mut f = FullscreenState::new(false);
|
||||
|
||||
|
After Width: | Height: | Size: 797 B |
@@ -20,6 +20,10 @@ pub mod fonts {
|
||||
pub const GEIST_MONO: &[u8] = include_bytes!("../fonts/GeistMono-Regular.ttf");
|
||||
}
|
||||
|
||||
/// Horizon's icon at 64 px (PNG), for window title bars and task bars at run time. Rendered from
|
||||
/// `docs/media/brand/icon.svg` by `cargo run -p xtask -- icons`.
|
||||
pub const ICON_PNG: &[u8] = include_bytes!("../icons/horizon-64.png");
|
||||
|
||||
/// How a type step sizes on the web.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Web {
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
# Horizon's mark and icons
|
||||
|
||||
**The mark.** An H whose crossbar is the horizon: the line runs edge to edge in Horizon's violet,
|
||||
the stems above it in full strength and their reflection below at a third. It is drawn on whole
|
||||
units of a 16-unit grid, so it is sharp at 16 px and holds at 1024 px. `docs/design-system.md`
|
||||
places it in the design system; `candidates/` keeps the two alternatives that were shown with it.
|
||||
|
||||
## Masters (edit these)
|
||||
|
||||
| File | What |
|
||||
|------|------|
|
||||
| `icon.svg` | the app icon on the 16 grid: the full square with 22 % corners (Windows, Linux, favicons) |
|
||||
| `icon-macos.svg` | the same tile on Apple's 1024 grid (824 px tile, 185 px corners, transparent margin) |
|
||||
| `avatar.svg` | full-bleed square for services that round or crop the image (GitHub, Discord) |
|
||||
| `avatar-kira.svg` | the bot's avatar: the mark on violet, so Kira reads apart from the project |
|
||||
| `mark.svg`, `mark-dark.svg` | the mark alone, for light and for dark backgrounds |
|
||||
| `social/card.html`, `social/banner.html` | the GitHub social preview and the Discord banner, as pages |
|
||||
|
||||
## Generated (don't edit)
|
||||
|
||||
`cargo run -p xtask -- icons` (needs `rsvg-convert` from librsvg) renders the masters into:
|
||||
|
||||
| Output | Used by |
|
||||
|--------|---------|
|
||||
| `packaging/icons/hicolor/<size>/apps/horizon.png` (16–512) and `scalable/apps/horizon.svg` | Linux: copy the tree into `~/.local/share/icons/hicolor` (or `/usr/share/icons/hicolor`) |
|
||||
| `packaging/linux/horizon-launcher.desktop`, `horizon.desktop` (hand-written) | Linux: `~/.local/share/applications`; Wayland matches the windows' app ids `horizon-launcher` and `horizon` |
|
||||
| `packaging/icons/horizon.ico` | Windows: embedded in `horizon-client.exe`, its tools and `horizon-launcher.exe` as icon resource 1 (`build.rs` of each, Windows builds only) |
|
||||
| `packaging/icons/horizon.icns` | macOS: an app bundle's `Contents/Resources/horizon.icns` (`CFBundleIconFile`) |
|
||||
| `crates/horizon-theme/icons/horizon-64.png` | the game window's icon at run time (X11 and Windows title bars, `window::icon`) |
|
||||
| `website/public/favicon.svg`, `favicon-32.png`, `apple-touch-icon.png` | the website |
|
||||
| `icon-1024.png`, `icon-macos-1024.png` | previews, store listings |
|
||||
| `social/github-avatar-500.png` | GitHub: the organisation or account avatar |
|
||||
| `social/discord-icon-512.png` | Discord: the server icon |
|
||||
| `social/kira-avatar-512.png` | Discord: Kira's avatar |
|
||||
|
||||
The two pages in `social/` are screenshots, at their exact size, with Playwright from the
|
||||
website's dev dependencies:
|
||||
|
||||
```sh
|
||||
cd website && npm install
|
||||
node -e 'const {chromium}=require("playwright");(async()=>{const b=await chromium.launch();for(const[f,w,h,o]of[["card",1280,640,"github-social-1280x640"],["banner",960,540,"discord-banner-960x540"]]){const p=await b.newPage({viewport:{width:w,height:h}});await p.goto("file://"+require("path").resolve("../docs/media/brand/social/"+f+".html"));await p.waitForTimeout(300);await p.screenshot({path:"../docs/media/brand/social/"+o+".png"})}await b.close()})()'
|
||||
```
|
||||
|
||||
## Uploading (the accounts are Ludovic's)
|
||||
|
||||
- **GitHub avatar**: github.com/settings/profile (or the organisation's settings), *Edit* the
|
||||
picture, upload `social/github-avatar-500.png`.
|
||||
- **GitHub social preview**: the repository's *Settings › General › Social preview › Edit*,
|
||||
upload `social/github-social-1280x640.png`.
|
||||
- **Discord server icon and banner**: *Server Settings › Server Profile*: icon
|
||||
`social/discord-icon-512.png`; banner `social/discord-banner-960x540.png` (the banner needs
|
||||
boost level 2).
|
||||
- **Kira's avatar**: discord.com/developers/applications › Kira › *Bot* › icon, upload
|
||||
`social/kira-avatar-512.png` (or *General Information › App Icon* for the application too).
|
||||
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<rect width="16" height="16" fill="#5b3fd6"/>
|
||||
<g transform="translate(2.4 2.4) scale(.7)">
|
||||
<g fill="#ffffff"><rect x="4" y="3" width="2" height="4"/><rect x="10" y="3" width="2" height="4"/></g>
|
||||
<g fill="#ffffff" opacity=".36"><rect x="4" y="8" width="2" height="4"/><rect x="10" y="8" width="2" height="4"/></g>
|
||||
</g>
|
||||
<rect y="7.3" width="16" height=".7" fill="#ffffff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 456 B |
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<rect width="16" height="16" fill="#16181f"/>
|
||||
<g transform="translate(2.4 2.4) scale(.7)">
|
||||
<g fill="#ecedf3"><rect x="4" y="3" width="2" height="4"/><rect x="10" y="3" width="2" height="4"/></g>
|
||||
<g fill="#ecedf3" opacity=".36"><rect x="4" y="8" width="2" height="4"/><rect x="10" y="8" width="2" height="4"/></g>
|
||||
</g>
|
||||
<rect y="7.3" width="16" height=".7" fill="#9a86ff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 456 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect width="16" height="16" rx="3.5" fill="#16181f"/><g fill="#ecedf3"><rect x="6" y="5" width="4" height="1"/><rect x="4" y="7" width="8" height="1"/><rect x="3" y="9" width="10" height="1"/></g><g fill="#9a86ff"><rect x="2" y="11" width="12" height="1"/></g></svg>
|
||||
|
After Width: | Height: | Size: 328 B |
@@ -0,0 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<title>Horizon</title>
|
||||
<clipPath id="tile"><rect width="16" height="16" rx="3.5"/></clipPath>
|
||||
<g clip-path="url(#tile)">
|
||||
<rect width="16" height="16" fill="#16181f"/>
|
||||
<g fill="#ecedf3"><rect x="4" y="3" width="2" height="4"/><rect x="10" y="3" width="2" height="4"/></g>
|
||||
<g fill="#ecedf3" opacity=".36"><rect x="4" y="8" width="2" height="4"/><rect x="10" y="8" width="2" height="4"/></g>
|
||||
<rect y="7" width="16" height="1" fill="#9a86ff"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 537 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><clipPath id="c"><rect width="16" height="16" rx="3.5"/></clipPath><g clip-path="url(#c)"><rect width="16" height="16" fill="#16181f"/><path d="M3 10a5 5 0 0 1 10 0z" fill="#ecedf3"/><rect y="10" width="16" height="6" fill="#20222c"/><rect y="10" width="16" height="1" fill="#9a86ff"/></g></svg>
|
||||
|
After Width: | Height: | Size: 356 B |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
||||
<title>Horizon</title>
|
||||
<clipPath id="tile"><rect x="100" y="100" width="824" height="824" rx="185"/></clipPath>
|
||||
<g clip-path="url(#tile)">
|
||||
<rect x="100" y="100" width="824" height="824" fill="#16181f"/>
|
||||
<g transform="translate(100 100) scale(51.5)">
|
||||
<g fill="#ecedf3"><rect x="4" y="3" width="2" height="4"/><rect x="10" y="3" width="2" height="4"/></g>
|
||||
<g fill="#ecedf3" opacity=".36"><rect x="4" y="8" width="2" height="4"/><rect x="10" y="8" width="2" height="4"/></g>
|
||||
<rect y="7" width="16" height="1" fill="#9a86ff"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 643 B |
@@ -0,0 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<title>Horizon</title>
|
||||
<clipPath id="tile"><rect width="16" height="16" rx="3.5"/></clipPath>
|
||||
<g clip-path="url(#tile)">
|
||||
<rect width="16" height="16" fill="#16181f"/>
|
||||
<g fill="#ecedf3"><rect x="4" y="3" width="2" height="4"/><rect x="10" y="3" width="2" height="4"/></g>
|
||||
<g fill="#ecedf3" opacity=".36"><rect x="4" y="8" width="2" height="4"/><rect x="10" y="8" width="2" height="4"/></g>
|
||||
<rect y="7" width="16" height="1" fill="#9a86ff"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 537 B |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<title>Horizon</title>
|
||||
<g fill="#ecedf3"><rect x="4" y="3" width="2" height="4"/><rect x="10" y="3" width="2" height="4"/></g>
|
||||
<g fill="#ecedf3" opacity=".36"><rect x="4" y="8" width="2" height="4"/><rect x="10" y="8" width="2" height="4"/></g>
|
||||
<rect x="1" y="7" width="14" height="1" fill="#9a86ff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 378 B |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<title>Horizon</title>
|
||||
<g fill="#16181f"><rect x="4" y="3" width="2" height="4"/><rect x="10" y="3" width="2" height="4"/></g>
|
||||
<g fill="#16181f" opacity=".36"><rect x="4" y="8" width="2" height="4"/><rect x="10" y="8" width="2" height="4"/></g>
|
||||
<rect x="1" y="7" width="14" height="1" fill="#5b3fd6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 378 B |
@@ -0,0 +1,19 @@
|
||||
<!doctype html><meta charset=utf-8><style>
|
||||
@font-face{font-family:Geist;src:url(/home/ludovic/.t3/worktrees/horizon/t3-1a6cb6c4/website/public/fonts/Geist-Regular.woff2);font-weight:400}
|
||||
@font-face{font-family:Geist;src:url(/home/ludovic/.t3/worktrees/horizon/t3-1a6cb6c4/website/public/fonts/Geist-Medium.woff2);font-weight:500}
|
||||
@font-face{font-family:"Geist Mono";src:url(/home/ludovic/.t3/worktrees/horizon/t3-1a6cb6c4/website/public/fonts/GeistMono-Regular.woff2)}
|
||||
html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#0d0e13;color:#ecedf3;font-family:Geist}
|
||||
.wrap{position:relative;height:100%;padding:var(--pad);box-sizing:border-box}
|
||||
.top{display:flex;align-items:center;gap:12px;font-size:var(--small);font-weight:500}
|
||||
.top img{width:calc(var(--small)*1.6)}
|
||||
.top span{margin-left:auto;font:400 calc(var(--small)*.85) "Geist Mono";color:#8b8f9c;letter-spacing:.04em}
|
||||
h1{margin:var(--gap) 0 0;font-weight:500;font-size:var(--display);line-height:1.05;letter-spacing:-.025em;max-width:16ch}
|
||||
p{margin:calc(var(--gap)*.45) 0 0;color:#8b8f9c;font-size:var(--body);line-height:1.5;max-width:40ch}
|
||||
.word{position:absolute;left:calc(var(--pad) - .06em);bottom:-.2em;font-weight:500;font-size:var(--word);letter-spacing:-.04em;line-height:.8;color:#181a22}
|
||||
.line{position:absolute;left:0;right:0;bottom:var(--line);height:2px;background:#9a86ff}
|
||||
</style><body style="--pad:48px;--small:18px;--gap:44px;--display:52px;--body:20px;--word:260px;--line:0px"><div class=wrap>
|
||||
<div class=word>horizon</div>
|
||||
<div class=top><img src=../icon.svg>Horizon<span>MINECRAFT: JAVA EDITION 26.3 · RUST</span></div>
|
||||
<h1>The game you know, rebuilt in Rust.</h1>
|
||||
<p>An unofficial client and game logic, checked against vanilla by differential harnesses.</p>
|
||||
</div></body>
|
||||
@@ -0,0 +1,19 @@
|
||||
<!doctype html><meta charset=utf-8><style>
|
||||
@font-face{font-family:Geist;src:url(/home/ludovic/.t3/worktrees/horizon/t3-1a6cb6c4/website/public/fonts/Geist-Regular.woff2);font-weight:400}
|
||||
@font-face{font-family:Geist;src:url(/home/ludovic/.t3/worktrees/horizon/t3-1a6cb6c4/website/public/fonts/Geist-Medium.woff2);font-weight:500}
|
||||
@font-face{font-family:"Geist Mono";src:url(/home/ludovic/.t3/worktrees/horizon/t3-1a6cb6c4/website/public/fonts/GeistMono-Regular.woff2)}
|
||||
html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#0d0e13;color:#ecedf3;font-family:Geist}
|
||||
.wrap{position:relative;height:100%;padding:var(--pad);box-sizing:border-box}
|
||||
.top{display:flex;align-items:center;gap:12px;font-size:var(--small);font-weight:500}
|
||||
.top img{width:calc(var(--small)*1.6)}
|
||||
.top span{margin-left:auto;font:400 calc(var(--small)*.85) "Geist Mono";color:#8b8f9c;letter-spacing:.04em}
|
||||
h1{margin:var(--gap) 0 0;font-weight:500;font-size:var(--display);line-height:1.05;letter-spacing:-.025em;max-width:16ch}
|
||||
p{margin:calc(var(--gap)*.45) 0 0;color:#8b8f9c;font-size:var(--body);line-height:1.5;max-width:40ch}
|
||||
.word{position:absolute;left:calc(var(--pad) - .06em);bottom:-.2em;font-weight:500;font-size:var(--word);letter-spacing:-.04em;line-height:.8;color:#181a22}
|
||||
.line{position:absolute;left:0;right:0;bottom:var(--line);height:2px;background:#9a86ff}
|
||||
</style><body style="--pad:64px;--small:20px;--gap:64px;--display:64px;--body:24px;--word:340px;--line:0px"><div class=wrap>
|
||||
<div class=word>horizon</div>
|
||||
<div class=top><img src=../icon.svg>Horizon<span>MINECRAFT: JAVA EDITION 26.3 · RUST</span></div>
|
||||
<h1>The game you know, rebuilt in Rust.</h1>
|
||||
<p>An unofficial client and game logic, checked against vanilla by differential harnesses.</p>
|
||||
</div></body>
|
||||
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
@@ -2584,6 +2584,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"tar",
|
||||
"ureq",
|
||||
"winresource",
|
||||
"zip",
|
||||
]
|
||||
|
||||
@@ -6937,6 +6938,16 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winresource"
|
||||
version = "0.1.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2bc6d36480b829272a5bdabf08e6dca8004761c1da7e35efef800540c66221b"
|
||||
dependencies = [
|
||||
"toml 1.1.8+spec-1.1.0",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winsafe"
|
||||
version = "0.0.19"
|
||||
|
||||
@@ -35,5 +35,9 @@ keyring = { version = "3", features = ["apple-native"] }
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
keyring = { version = "3", features = ["windows-native"] }
|
||||
|
||||
[target.'cfg(windows)'.build-dependencies]
|
||||
# The icon resource in the .exe (build.rs).
|
||||
winresource = "0.1"
|
||||
|
||||
[profile.release]
|
||||
strip = true
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
//! Embeds Horizon's icon (`packaging/icons/horizon.ico`) in the Windows executables, as icon
|
||||
//! resource 1: Explorer and the task bar show it, and the window takes it as its icon.
|
||||
fn main() {
|
||||
let dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_default();
|
||||
let ico = format!("{dir}/../packaging/icons/horizon.ico");
|
||||
println!("cargo:rerun-if-changed={ico}");
|
||||
#[cfg(windows)]
|
||||
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows") {
|
||||
let mut resource = winresource::WindowsResource::new();
|
||||
resource.set_icon(&ico);
|
||||
if let Err(e) = resource.compile() {
|
||||
panic!("embedding {ico}: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 280 B |
|
After Width: | Height: | Size: 390 B |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 424 B |
|
After Width: | Height: | Size: 616 B |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 797 B |
@@ -0,0 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<title>Horizon</title>
|
||||
<clipPath id="tile"><rect width="16" height="16" rx="3.5"/></clipPath>
|
||||
<g clip-path="url(#tile)">
|
||||
<rect width="16" height="16" fill="#16181f"/>
|
||||
<g fill="#ecedf3"><rect x="4" y="3" width="2" height="4"/><rect x="10" y="3" width="2" height="4"/></g>
|
||||
<g fill="#ecedf3" opacity=".36"><rect x="4" y="8" width="2" height="4"/><rect x="10" y="8" width="2" height="4"/></g>
|
||||
<rect y="7" width="16" height="1" fill="#9a86ff"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 537 B |
|
After Width: | Height: | Size: 7.5 KiB |
@@ -0,0 +1,10 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Horizon
|
||||
GenericName=Game launcher
|
||||
Comment=Install, prepare and play Horizon, Minecraft: Java Edition 26.3 rewritten in Rust
|
||||
Exec=horizon-launcher
|
||||
Icon=horizon
|
||||
Terminal=false
|
||||
Categories=Game;
|
||||
StartupWMClass=horizon-launcher
|
||||
@@ -0,0 +1,10 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Horizon
|
||||
Comment=Minecraft: Java Edition 26.3, rewritten in Rust
|
||||
Exec=horizon-client
|
||||
Icon=horizon
|
||||
Terminal=false
|
||||
Categories=Game;
|
||||
StartupWMClass=horizon
|
||||
NoDisplay=true
|
||||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 733 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 424 B |
@@ -1,62 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<title>Horizon</title>
|
||||
<defs>
|
||||
<linearGradient id="sky" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="#0A0F2C"/>
|
||||
<stop offset="0.38" stop-color="#26206A"/>
|
||||
<stop offset="0.62" stop-color="#7C3A88"/>
|
||||
<stop offset="0.78" stop-color="#EE6E5A"/>
|
||||
<stop offset="0.9" stop-color="#FFB25E"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="sun" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="#FFF6CF"/>
|
||||
<stop offset="0.5" stop-color="#FFD272"/>
|
||||
<stop offset="1" stop-color="#FF9A44"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="glow" cx="0.5" cy="0.6" r="0.55">
|
||||
<stop offset="0" stop-color="#FFC874" stop-opacity="0.6"/>
|
||||
<stop offset="0.6" stop-color="#FF8A5C" stop-opacity="0.12"/>
|
||||
<stop offset="1" stop-color="#FF8A5C" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="far" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="#4A2F7E"/>
|
||||
<stop offset="1" stop-color="#2B1F5E"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="near" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="#151A45"/>
|
||||
<stop offset="1" stop-color="#080B22"/>
|
||||
</linearGradient>
|
||||
<clipPath id="tile"><rect width="512" height="512" rx="112"/></clipPath>
|
||||
<clipPath id="sunCut"><rect x="0" y="0" width="512" height="352"/></clipPath>
|
||||
<clipPath id="sunDisc"><circle cx="256" cy="300" r="136"/></clipPath>
|
||||
</defs>
|
||||
<clipPath id="tile"><rect width="16" height="16" rx="3.5"/></clipPath>
|
||||
<g clip-path="url(#tile)">
|
||||
<rect width="512" height="512" fill="url(#sky)"/>
|
||||
<ellipse cx="256" cy="320" rx="300" ry="230" fill="url(#glow)"/>
|
||||
<g clip-path="url(#sunCut)">
|
||||
<circle cx="256" cy="300" r="136" fill="url(#sun)"/>
|
||||
<g clip-path="url(#sunDisc)">
|
||||
<rect x="100" y="262" width="312" height="8" fill="#EE6E5A" opacity="0.5"/>
|
||||
<rect x="100" y="284" width="312" height="11" fill="#EE6E5A" opacity="0.62"/>
|
||||
<rect x="100" y="310" width="312" height="14" fill="#EE6E5A" opacity="0.74"/>
|
||||
</g>
|
||||
</g>
|
||||
<path fill="url(#far)" d="M0 320H64V288H128V320H352V288H416V256H512V512H0Z"/>
|
||||
<path fill="url(#near)" d="M0 384H64V352H192V384H320V352H384V320H448V352H512V512H0Z"/>
|
||||
<g fill="#FFFFFF" opacity="0.07">
|
||||
<rect x="64" y="352" width="4" height="160"/>
|
||||
<rect x="192" y="352" width="4" height="160"/>
|
||||
<rect x="320" y="352" width="4" height="160"/>
|
||||
<rect x="384" y="320" width="4" height="192"/>
|
||||
<rect x="448" y="320" width="4" height="192"/>
|
||||
<rect x="0" y="448" width="512" height="4"/>
|
||||
</g>
|
||||
<rect x="0" y="384" width="64" height="4" fill="#FFB25E" opacity="0.25"/>
|
||||
<rect x="64" y="352" width="128" height="4" fill="#FFB25E" opacity="0.35"/>
|
||||
<rect x="192" y="384" width="128" height="4" fill="#FFB25E" opacity="0.25"/>
|
||||
<rect x="320" y="352" width="64" height="4" fill="#FFB25E" opacity="0.35"/>
|
||||
<rect x="384" y="320" width="64" height="4" fill="#FFB25E" opacity="0.45"/>
|
||||
<rect x="448" y="352" width="64" height="4" fill="#FFB25E" opacity="0.35"/>
|
||||
<rect width="16" height="16" fill="#16181f"/>
|
||||
<g fill="#ecedf3"><rect x="4" y="3" width="2" height="4"/><rect x="10" y="3" width="2" height="4"/></g>
|
||||
<g fill="#ecedf3" opacity=".36"><rect x="4" y="8" width="2" height="4"/><rect x="10" y="8" width="2" height="4"/></g>
|
||||
<rect y="7" width="16" height="1" fill="#9a86ff"/>
|
||||
</g>
|
||||
<rect x="2" y="2" width="508" height="508" rx="110" fill="none" stroke="#FFFFFF" stroke-opacity="0.1" stroke-width="4"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 537 B |
@@ -8,12 +8,16 @@
|
||||
//!
|
||||
//! `tokens`: writes `website/src/tokens.css`, the design tokens of `crates/horizon-theme` as CSS
|
||||
//! custom properties (a horizon-theme test fails when the file is out of date).
|
||||
//!
|
||||
//! `icons`: renders the icon masters in `docs/media/brand` (with `rsvg-convert`, from librsvg)
|
||||
//! into every size the platforms and the website use, and packs `packaging/icons/horizon.ico`
|
||||
//! and `horizon.icns`. `docs/media/brand/README.md` lists what goes where.
|
||||
|
||||
use std::fs;
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
use std::process::{Command, ExitCode};
|
||||
|
||||
const USAGE: &str = "usage: cargo run -p xtask -- package BINARY OUTDIR | tokens";
|
||||
const USAGE: &str = "usage: cargo run -p xtask -- package BINARY OUTDIR | tokens | icons";
|
||||
|
||||
/// What the release folder takes from the repository at HEAD, as `git archive` writes it.
|
||||
const ARCHIVED: [&str; 5] = [
|
||||
@@ -34,6 +38,7 @@ fn main() -> ExitCode {
|
||||
let result = match args.as_slice() {
|
||||
[task, binary, out] if task == "package" => package(Path::new(binary), Path::new(out)),
|
||||
[task] if task == "tokens" => tokens(),
|
||||
[task] if task == "icons" => icons(),
|
||||
_ => Err(USAGE.to_string()),
|
||||
};
|
||||
match result {
|
||||
@@ -69,6 +74,145 @@ fn tokens() -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The square icon's sizes: Linux's hicolor theme, and the ones the .ico holds.
|
||||
const HICOLOR: [u32; 8] = [16, 24, 32, 48, 64, 128, 256, 512];
|
||||
const ICO: [u32; 7] = [16, 24, 32, 48, 64, 128, 256];
|
||||
/// The .icns entries: type and pixel size, macOS's tile drawn on Apple's grid.
|
||||
const ICNS: [(&[u8; 4], u32); 11] = [
|
||||
(b"icp4", 16),
|
||||
(b"icp5", 32),
|
||||
(b"icp6", 64),
|
||||
(b"ic07", 128),
|
||||
(b"ic08", 256),
|
||||
(b"ic09", 512),
|
||||
(b"ic10", 1024),
|
||||
(b"ic11", 32),
|
||||
(b"ic12", 64),
|
||||
(b"ic13", 256),
|
||||
(b"ic14", 512),
|
||||
];
|
||||
|
||||
fn render(svg: &Path, size: (u32, u32)) -> Result<Vec<u8>, String> {
|
||||
let output = Command::new("rsvg-convert")
|
||||
.arg("-w")
|
||||
.arg(size.0.to_string())
|
||||
.arg("-h")
|
||||
.arg(size.1.to_string())
|
||||
.arg(svg)
|
||||
.output()
|
||||
.map_err(|e| format!("rsvg-convert (librsvg): {e}"))?;
|
||||
if !output.status.success() {
|
||||
return Err(format!(
|
||||
"rsvg-convert {}: {}",
|
||||
svg.display(),
|
||||
String::from_utf8_lossy(&output.stderr).trim()
|
||||
));
|
||||
}
|
||||
Ok(output.stdout)
|
||||
}
|
||||
|
||||
fn write(path: &Path, data: &[u8]) -> Result<(), String> {
|
||||
if let Some(dir) = path.parent() {
|
||||
fs::create_dir_all(dir).map_err(|e| format!("{}: {e}", dir.display()))?;
|
||||
}
|
||||
fs::write(path, data).map_err(|e| format!("{}: {e}", path.display()))
|
||||
}
|
||||
|
||||
/// An .ico whose images are PNGs (Windows Vista and later read them at every size).
|
||||
fn ico(images: &[(u32, Vec<u8>)]) -> Vec<u8> {
|
||||
let mut out = vec![0, 0, 1, 0];
|
||||
out.extend((images.len() as u16).to_le_bytes());
|
||||
let mut offset = 6 + 16 * images.len();
|
||||
for (size, png) in images {
|
||||
// A width or height of 256 is written as 0.
|
||||
let side = if *size >= 256 { 0 } else { *size as u8 };
|
||||
out.extend([side, side, 0, 0]);
|
||||
out.extend(1u16.to_le_bytes());
|
||||
out.extend(32u16.to_le_bytes());
|
||||
out.extend((png.len() as u32).to_le_bytes());
|
||||
out.extend((offset as u32).to_le_bytes());
|
||||
offset += png.len();
|
||||
}
|
||||
for (_, png) in images {
|
||||
out.extend(png);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// An .icns of PNG entries.
|
||||
fn icns(entries: &[(&[u8; 4], Vec<u8>)]) -> Vec<u8> {
|
||||
let body: usize = entries.iter().map(|(_, png)| 8 + png.len()).sum();
|
||||
let mut out = b"icns".to_vec();
|
||||
out.extend(((8 + body) as u32).to_be_bytes());
|
||||
for (kind, png) in entries {
|
||||
out.extend(*kind);
|
||||
out.extend(((8 + png.len()) as u32).to_be_bytes());
|
||||
out.extend(png);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn icons() -> Result<(), String> {
|
||||
let repo = Path::new(env!("CARGO_MANIFEST_DIR")).join("..");
|
||||
let brand = repo.join("docs/media/brand");
|
||||
let square = brand.join("icon.svg");
|
||||
let packaging = repo.join("packaging/icons");
|
||||
for size in HICOLOR {
|
||||
let png = render(&square, (size, size))?;
|
||||
write(
|
||||
&packaging.join(format!("hicolor/{size}x{size}/apps/horizon.png")),
|
||||
&png,
|
||||
)?;
|
||||
}
|
||||
write(
|
||||
&packaging.join("hicolor/scalable/apps/horizon.svg"),
|
||||
&fs::read(&square).map_err(|e| format!("{}: {e}", square.display()))?,
|
||||
)?;
|
||||
let images = ICO
|
||||
.iter()
|
||||
.map(|&s| render(&square, (s, s)).map(|png| (s, png)))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
write(&packaging.join("horizon.ico"), &ico(&images))?;
|
||||
let macos = brand.join("icon-macos.svg");
|
||||
let entries = ICNS
|
||||
.iter()
|
||||
.map(|&(kind, s)| render(&macos, (s, s)).map(|png| (kind, png)))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
write(&packaging.join("horizon.icns"), &icns(&entries))?;
|
||||
// The window icon the game and the launcher set at run time (X11 and Windows title bars).
|
||||
write(
|
||||
&repo.join("crates/horizon-theme/icons/horizon-64.png"),
|
||||
&render(&square, (64, 64))?,
|
||||
)?;
|
||||
// Avatars and the website's icons.
|
||||
let avatar = brand.join("avatar.svg");
|
||||
for (svg, size, out) in [
|
||||
(
|
||||
&avatar,
|
||||
500,
|
||||
"docs/media/brand/social/github-avatar-500.png",
|
||||
),
|
||||
(&avatar, 512, "docs/media/brand/social/discord-icon-512.png"),
|
||||
(
|
||||
&brand.join("avatar-kira.svg"),
|
||||
512,
|
||||
"docs/media/brand/social/kira-avatar-512.png",
|
||||
),
|
||||
(&avatar, 180, "website/public/apple-touch-icon.png"),
|
||||
(&square, 32, "website/public/favicon-32.png"),
|
||||
(&square, 1024, "docs/media/brand/icon-1024.png"),
|
||||
(&macos, 1024, "docs/media/brand/icon-macos-1024.png"),
|
||||
] {
|
||||
write(&repo.join(out), &render(svg, (size, size))?)?;
|
||||
}
|
||||
write(
|
||||
&repo.join("website/public/favicon.svg"),
|
||||
&fs::read(&square).map_err(|e| format!("{}: {e}", square.display()))?,
|
||||
)?;
|
||||
println!("wrote packaging/icons, the window icon, the avatars and the website's icons");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn package(binary: &Path, out: &Path) -> Result<(), String> {
|
||||
let top = git(
|
||||
Path::new(env!("CARGO_MANIFEST_DIR")),
|
||||
|
||||