Library changes outside the platform's own dependencies (those came with the platform commits above). libs/asset and libs/flow move to the Stage repository with the apps that use them. Covers work ebf442a94..2b1a41df4 for libs: - wm: the Android super-app compiles tiles on the phone against the host engine - cargo-makepad: the super-app APK is packed by `android dyn-pack`, in Rust - cef: a page's audio can be captured instead of played - flow: the flow-ui app and the flowgraph canvas return to the workspace, with staged progress in libs/flow - svg: a stroke that bends tighter than its half width collapses its inner edge to the corner - the accent a name is written with stops deciding whether search finds it - ai stems: LaneDemixer, VocalsModel and the platform Stage origin/main needs - The regex engine accepts \b and \B, reports the earliest match so a scan can resume after it, and can decide word boundaries on ASCII so a code sear - workspace: Scope's settings carry the code map's tab width and the directories each prepared project hides from its map - cef, video, platform: what Stage's browser needs from the shared layers - platform, wm: work builds again on iOS, tvOS and wasm, and the window manager is warning-free on Windows - ci: tools/ci, a Splash runtime that watches a branch and runs every ci.splash it finds - stitch: the crate says what the rest of the repository says about its license - ci: a run is one target dir and one cargo batch per target, and the wall draws - apps: what the CI box found - Remove Flow, asset and VJ applications moved into Stage - tests: the workspace suite, run as a whole for the first time, passes outside the example UI tests - libs: no warnings in the workspace check on any row - tests: the last three failures of the CI box's night - gif: the crate's doc examples compile - cef: a hosted browser survives its host's exit - tests: the three binaries over ten seconds in release come under it - git: the memory ledger can take what it is asked for - script: re-entrant dispatch, thread index validation, Any-based handle downcasts, UTF-8 previews; regex: never_loop fix - fix(text): enable ttf-parser gvar-alloc for many-tuple variable fonts - platform, widgets, wm: a touch that is taken away is cancelled, never released - platform, wm: hosted apps ask the WM for what they cannot do themselves - wm on Android: apps build on the phone - task, cargo-makepad, widgets, audio: the task manager chooses its columns, graphs every process' network and disk traffic and installs itself as a D - flowgraph: ordered ports take many wires, sockets can override their icon, and the canvas embeds as a viewport - cef: captured frames and audio carry callback clocks and a navigation epoch - git: timings read a portable clock - ai: the hub's rig-fixture tests find the asset library without the asset client - ai llm: a Metal main buffer is filled in 32 MB chunks - diskmap: a scan lists each macOS directory in one getattrlistbulk call and classifies files without allocating or locking - platform, widgets, audio_route, ai: window crossfades and whole-frame presents, caption controls that click, themed menus, a stereo-pair audio tap, - builder: the Builder TUI matches the new terminal design, Windows executables carry app icons, and every app has one - sheets, score, git, home: saves cannot cut a file short or overwrite another one, and checkouts cannot write outside the repository - git: worktree status matches git on real repositories and can be cancelled - cuda: builds link only the CUDA toolkit they are given, never a system install - audio_route: one API on macOS, Windows and Linux, monitoring by default and processing only when asked, plus shaders that discard a value compile to - platform, widgets, ai: a restyle recompiles nothing it already has, shows only complete frames, and a window can own its caption - audio_route example: print the samples that reached the processor each second - widgets, builder: the big widget families are features, all on by default, and the Builder takes none of them - ai hub: a local chat with no model says so once, plainly - ai: Claude Desktop drives any app with the F10 panel - wm: children open no console windows on Windows - ai: Connect hands the .mcpb to Claude Desktop itself - civil-time: one local wall clock for every app, and it knows the zone on Windows - ai-speech: makepad-ai-sfx (and with it ai-h3) is built only for IndexTTS - script + wasm_bridge: no build script that reruns on every file - build: the AI stack, csg, Scope and Source Library join the one workspace - ai chat: the chat's backends are the CLI, MCP and cloud providers; the local model is the `localai` feature - widgets: every widget family is a crate of its own under widgets/families, and makepad-widgets links, re-exports and registers the ones its features - apps, examples, libs: each crate builds only the widget families it uses, and the design overlay is each app's own default feature - livepipe: takes makepad-widgets without its default families, now that the AI stack is in the one workspace - ai chat: a build without local AI starts on the first provider that answers here, not on "No model" - thiserror: RUSTC_BOOTSTRAP no longer turns on the unstable generic member access API - builder: makepad-builder.exe declares itself like a well-formed Windows program and stops a stalled build through a job object instead of taskkill - builder + ai-cuda: the CUDA kernel progress shows in the Builder's Compile row, not scribbled over its screen - repo: no third-party comic archive, generated models or sample asset in the tree - builder: the Windows Builder ships as source, runs in its own console, and apps compile with Rust's GNU toolchain or Microsoft's - win_resource: PNG decode and encode on makepad-fast-inflate instead of zune-png - builder: one folder layout everywhere, makepad-builder.zip with only the files a Windows build reads, a Rust download over parallel connections, and Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
221 lines
9.3 KiB
Rust
221 lines
9.3 KiB
Rust
//! Windows icon images: the per-size `RT_ICON` payloads (32-bit DIB, or PNG
|
||
//! for 256 px), the `RT_GROUP_ICON` directory that names them, and the .ico
|
||
//! file format for reading existing icons.
|
||
use crate::icns::{decode_icns, SourceImage};
|
||
use crate::image::{Image, PNG_SIGNATURE};
|
||
|
||
/// Sizes Explorer, the taskbar and Alt-Tab ask for at 100–200 % scaling.
|
||
pub const ICON_SIZES: [u32; 7] = [16, 24, 32, 48, 64, 128, 256];
|
||
|
||
#[derive(Clone, Debug, PartialEq)]
|
||
pub struct IconImage {
|
||
pub width: u32,
|
||
pub height: u32,
|
||
/// A PNG file, or a BITMAPINFOHEADER DIB with doubled height followed by
|
||
/// the colour pixels and the 1-bit AND mask.
|
||
pub data: Vec<u8>,
|
||
}
|
||
|
||
impl IconImage {
|
||
pub fn is_png(&self) -> bool {
|
||
self.data.starts_with(PNG_SIGNATURE)
|
||
}
|
||
pub fn decode(&self) -> Result<Image, String> {
|
||
if self.is_png() { Image::decode_png(&self.data) } else { decode_dib(&self.data) }
|
||
}
|
||
}
|
||
|
||
#[derive(Clone, Debug, PartialEq)]
|
||
pub struct GroupEntry {
|
||
pub width: u32,
|
||
pub height: u32,
|
||
pub bit_count: u16,
|
||
pub bytes: u32,
|
||
pub id: u16,
|
||
}
|
||
|
||
/// Decode an icon source by content: `.icns`, a single PNG, or `.ico`.
|
||
pub fn load_source(bytes: &[u8]) -> Result<Vec<SourceImage>, String> {
|
||
let images = if bytes.starts_with(b"icns") {
|
||
decode_icns(bytes)?
|
||
} else if bytes.starts_with(PNG_SIGNATURE) {
|
||
vec![SourceImage { image: Image::decode_png(bytes)?, png: Some(bytes.to_vec()) }]
|
||
} else if bytes.starts_with(&[0, 0, 1, 0]) {
|
||
let mut images = Vec::new();
|
||
for icon in parse_ico(bytes)? {
|
||
let png = icon.is_png().then(|| icon.data.clone());
|
||
images.push(SourceImage { image: icon.decode()?, png });
|
||
}
|
||
images
|
||
} else {
|
||
return Err("unsupported icon source: expected .icns, .png or .ico".into());
|
||
};
|
||
if images.is_empty() {
|
||
return Err("icon source holds no pictures we can read".into());
|
||
}
|
||
Ok(images)
|
||
}
|
||
|
||
/// The icon's pictures for each of `ICON_SIZES` up to the largest source.
|
||
/// Exact source sizes are used as they are; others are reduced from the
|
||
/// smallest larger source. 256 px is stored as PNG, the rest as DIBs, which
|
||
/// every Windows version and icon consumer reads.
|
||
pub fn icon_images(sources: &[SourceImage]) -> Result<Vec<IconImage>, String> {
|
||
let side = |s: &SourceImage| s.image.width.min(s.image.height);
|
||
let largest = sources.iter().map(side).max().ok_or("icon source holds no pictures")?;
|
||
let mut sizes: Vec<u32> = ICON_SIZES.iter().copied().filter(|&s| s <= largest).collect();
|
||
if sizes.is_empty() {
|
||
sizes.push(largest);
|
||
}
|
||
let mut images = Vec::new();
|
||
for size in sizes {
|
||
let exact = sources.iter().find(|s| s.image.width == size && s.image.height == size);
|
||
if size >= 256 {
|
||
if let Some(png) = exact.and_then(|s| s.png.clone()) {
|
||
images.push(IconImage { width: size, height: size, data: png });
|
||
continue;
|
||
}
|
||
}
|
||
let image = match exact {
|
||
Some(source) => source.image.clone(),
|
||
None => {
|
||
let from = sources.iter().filter(|s| side(s) >= size).min_by_key(|s| side(s)).unwrap();
|
||
from.image.resample(size, size)
|
||
}
|
||
};
|
||
let data = if size >= 256 { image.encode_png()? } else { encode_dib(&image) };
|
||
images.push(IconImage { width: size, height: size, data });
|
||
}
|
||
Ok(images)
|
||
}
|
||
|
||
pub fn encode_dib(image: &Image) -> Vec<u8> {
|
||
let (w, h) = (image.width as usize, image.height as usize);
|
||
let mask_stride = (w + 31) / 32 * 4;
|
||
let mut out = Vec::with_capacity(40 + w * h * 4 + mask_stride * h);
|
||
for v in [40u32, w as u32, (h * 2) as u32] { out.extend_from_slice(&v.to_le_bytes()); }
|
||
out.extend_from_slice(&1u16.to_le_bytes());
|
||
out.extend_from_slice(&32u16.to_le_bytes());
|
||
out.extend_from_slice(&0u32.to_le_bytes());
|
||
out.extend_from_slice(&((w * h * 4 + mask_stride * h) as u32).to_le_bytes());
|
||
out.extend_from_slice(&[0; 16]);
|
||
for y in (0..h).rev() {
|
||
for p in image.rgba[y * w * 4..(y + 1) * w * 4].chunks_exact(4) {
|
||
out.extend_from_slice(&[p[2], p[1], p[0], p[3]]);
|
||
}
|
||
}
|
||
// The AND mask marks fully transparent pixels for consumers that ignore
|
||
// the alpha channel.
|
||
for y in (0..h).rev() {
|
||
let mut row = vec![0u8; mask_stride];
|
||
for x in 0..w {
|
||
if image.rgba[(y * w + x) * 4 + 3] == 0 { row[x / 8] |= 0x80 >> (x % 8); }
|
||
}
|
||
out.extend_from_slice(&row);
|
||
}
|
||
out
|
||
}
|
||
|
||
/// Read a 32- or 24-bit icon DIB (the formats `encode_dib` and common icon
|
||
/// editors write).
|
||
pub fn decode_dib(data: &[u8]) -> Result<Image, String> {
|
||
let u32_at = |at: usize| data.get(at..at + 4).map(|b| u32::from_le_bytes(b.try_into().unwrap()));
|
||
let header = u32_at(0).ok_or("DIB truncated")? as usize;
|
||
let w = u32_at(4).ok_or("DIB truncated")? as i32;
|
||
let h2 = u32_at(8).ok_or("DIB truncated")? as i32;
|
||
let bits = data.get(14..16).map(|b| u16::from_le_bytes(b.try_into().unwrap())).ok_or("DIB truncated")?;
|
||
let compression = u32_at(16).ok_or("DIB truncated")?;
|
||
if header < 40 || w <= 0 || h2 <= 0 || w > 1024 || h2 > 2048 || compression != 0 || !(bits == 32 || bits == 24) {
|
||
return Err(format!("unsupported icon DIB: {w}x{h2}, {bits} bit, compression {compression}"));
|
||
}
|
||
let (w, h) = (w as usize, h2 as usize / 2);
|
||
let stride = (w * bits as usize / 8 + 3) / 4 * 4;
|
||
let mask_stride = (w + 31) / 32 * 4;
|
||
let pixels = data.get(header..header + stride * h).ok_or("DIB pixels truncated")?;
|
||
let mask = data.get(header + stride * h..header + stride * h + mask_stride * h);
|
||
let mut rgba = vec![0u8; w * h * 4];
|
||
for y in 0..h {
|
||
let row = &pixels[(h - 1 - y) * stride..];
|
||
for x in 0..w {
|
||
let p = &row[x * bits as usize / 8..];
|
||
let alpha = if bits == 32 {
|
||
p[3]
|
||
} else {
|
||
let masked = mask.map_or(false, |m| m[(h - 1 - y) * mask_stride + x / 8] & (0x80 >> (x % 8)) != 0);
|
||
if masked { 0 } else { 255 }
|
||
};
|
||
rgba[(y * w + x) * 4..(y * w + x + 1) * 4].copy_from_slice(&[p[2], p[1], p[0], alpha]);
|
||
}
|
||
}
|
||
Image::new(w as u32, h as u32, rgba)
|
||
}
|
||
|
||
fn dir_dimension(v: u32) -> u8 {
|
||
if v >= 256 { 0 } else { v as u8 }
|
||
}
|
||
|
||
/// `GRPICONDIR`: the `RT_GROUP_ICON` payload naming `RT_ICON` resources
|
||
/// `first_id..`.
|
||
pub fn group_icon(images: &[IconImage], first_id: u16) -> Vec<u8> {
|
||
let mut out = Vec::with_capacity(6 + 14 * images.len());
|
||
for v in [0u16, 1, images.len() as u16] { out.extend_from_slice(&v.to_le_bytes()); }
|
||
for (i, image) in images.iter().enumerate() {
|
||
out.extend_from_slice(&[dir_dimension(image.width), dir_dimension(image.height), 0, 0]);
|
||
out.extend_from_slice(&1u16.to_le_bytes());
|
||
out.extend_from_slice(&32u16.to_le_bytes());
|
||
out.extend_from_slice(&(image.data.len() as u32).to_le_bytes());
|
||
out.extend_from_slice(&(first_id + i as u16).to_le_bytes());
|
||
}
|
||
out
|
||
}
|
||
|
||
pub fn parse_group_icon(data: &[u8]) -> Result<Vec<GroupEntry>, String> {
|
||
let u16_at = |at: usize| data.get(at..at + 2).map(|b| u16::from_le_bytes(b.try_into().unwrap())).ok_or("group icon truncated");
|
||
if u16_at(0)? != 0 || u16_at(2)? != 1 {
|
||
return Err("not a group icon directory".into());
|
||
}
|
||
let count = u16_at(4)? as usize;
|
||
(0..count).map(|i| {
|
||
let e = data.get(6 + i * 14..6 + (i + 1) * 14).ok_or("group icon entry truncated")?;
|
||
let dim = |b: u8| if b == 0 { 256 } else { b as u32 };
|
||
Ok(GroupEntry {
|
||
width: dim(e[0]),
|
||
height: dim(e[1]),
|
||
bit_count: u16::from_le_bytes([e[6], e[7]]),
|
||
bytes: u32::from_le_bytes(e[8..12].try_into().unwrap()),
|
||
id: u16::from_le_bytes([e[12], e[13]]),
|
||
})
|
||
}).collect()
|
||
}
|
||
|
||
pub fn write_ico(images: &[IconImage]) -> Vec<u8> {
|
||
let mut out = Vec::new();
|
||
for v in [0u16, 1, images.len() as u16] { out.extend_from_slice(&v.to_le_bytes()); }
|
||
let mut offset = 6 + 16 * images.len();
|
||
for image in images {
|
||
out.extend_from_slice(&[dir_dimension(image.width), dir_dimension(image.height), 0, 0]);
|
||
out.extend_from_slice(&1u16.to_le_bytes());
|
||
out.extend_from_slice(&32u16.to_le_bytes());
|
||
out.extend_from_slice(&(image.data.len() as u32).to_le_bytes());
|
||
out.extend_from_slice(&(offset as u32).to_le_bytes());
|
||
offset += image.data.len();
|
||
}
|
||
for image in images { out.extend_from_slice(&image.data); }
|
||
out
|
||
}
|
||
|
||
pub fn parse_ico(bytes: &[u8]) -> Result<Vec<IconImage>, String> {
|
||
let u16_at = |at: usize| bytes.get(at..at + 2).map(|b| u16::from_le_bytes(b.try_into().unwrap())).ok_or(".ico truncated");
|
||
if u16_at(0)? != 0 || u16_at(2)? != 1 {
|
||
return Err("not an .ico file".into());
|
||
}
|
||
let count = u16_at(4)? as usize;
|
||
(0..count).map(|i| {
|
||
let e = bytes.get(6 + i * 16..6 + (i + 1) * 16).ok_or(".ico entry truncated")?;
|
||
let size = u32::from_le_bytes(e[8..12].try_into().unwrap()) as usize;
|
||
let offset = u32::from_le_bytes(e[12..16].try_into().unwrap()) as usize;
|
||
let data = bytes.get(offset..offset.checked_add(size).ok_or(".ico entry overflows")?).ok_or(".ico image outside the file")?.to_vec();
|
||
let dim = |b: u8| if b == 0 { 256 } else { b as u32 };
|
||
Ok(IconImage { width: dim(e[0]), height: dim(e[1]), data })
|
||
}).collect()
|
||
}
|