Merge remote-tracking branch 'upstream/main' into lang-2-ui

# Conflicts:
#	Cargo.lock
This commit is contained in:
aiongg committed 2026-10-26 01:55:42 +00:00
commit fdd6ad530e
9 files changed
+173 -29

No files matched your search

Generated
+6 -6
View File
@@ -915,8 +915,8 @@ name = "craft-print"
version = "0.1.0"
dependencies = [
"craft-winprint-gdi",
"miniz_oxide 0.8.9",
"roxmltree 0.20.0",
"miniz_oxide 0.9.1",
"roxmltree 0.21.2",
"tempfile",
"winprint",
"winsafe",
@@ -1614,9 +1614,9 @@ checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
[[package]]
name = "fixed_decimal"
version = "0.7.2"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79c3c892f121fff406e5dd6b28c1b30096b95111c30701a899d4f2b18da6d1bd"
checksum = "2214fb00f669ee96bee5bb29dfb1ddabe058b2bc426817efc79afa03c9bc55ae"
dependencies = [
"displaydoc",
"smallvec",
@@ -4620,9 +4620,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.151"
version = "1.0.154"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
dependencies = [
"itoa",
"memchr",
+4 -4
View File
@@ -27,11 +27,11 @@ winsafe = { version = "0.0.29", default-features = false, features = ["winspool"
# The job file's private folder.
tempfile = "3"
# Reading the PrintTicket and PrintCapabilities the driver hands back.
roxmltree = "0.20"
roxmltree = "0.21"
# Inflating the print-ready PDF's cross-reference and object streams (src/sheets.rs).
miniz_oxide = "0.8"
miniz_oxide = "0.9"
[dev-dependencies]
tempfile = "3"
roxmltree = "0.20"
miniz_oxide = "0.8"
roxmltree = "0.21"
miniz_oxide = "0.9"
+57 -8
View File
@@ -517,6 +517,53 @@ fn indirect<'a>(data: &'a [u8], offset: usize, budget: &mut Budget) -> Option<In
Some(Indirect { num, obj, stream })
}
/// How an inflate ended.
enum Inflated {
/// The whole stream, within the limit.
Done(Vec<u8>),
/// The output would pass the limit.
Exhausted,
/// A damaged or truncated stream: only the bytes actually written before the damage.
Damaged(Vec<u8>),
}
/// Inflate `raw` (zlib-wrapped or bare deflate) to at most `limit` bytes. Unlike the
/// `decompress_to_vec*` helpers it reports the bytes the decompressor really wrote, so a stream
/// rejected early yields an empty `Damaged`, never the zero-filled output buffer.
fn inflate(raw: &[u8], zlib: bool, limit: usize) -> Inflated {
use miniz_oxide::inflate::TINFLStatus;
use miniz_oxide::inflate::core::{DecompressorOxide, decompress, inflate_flags};
let flags = inflate_flags::TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF | if zlib { inflate_flags::TINFL_FLAG_PARSE_ZLIB_HEADER } else { 0 };
let mut out = vec![0u8; raw.len().saturating_mul(2).max(64).min(limit)];
let mut state = Box::<DecompressorOxide>::default();
let (mut input, mut written) = (raw, 0usize);
loop {
let (status, consumed, wrote) = decompress(&mut state, input, &mut out, written, flags);
written = written.saturating_add(wrote).min(out.len());
match status {
TINFLStatus::Done => {
out.truncate(written);
return Inflated::Done(out);
}
TINFLStatus::HasMoreOutput => {
if out.len() >= limit {
return Inflated::Exhausted;
}
let Some(rest) = input.get(consumed..) else {
out.truncate(written);
return Inflated::Damaged(out);
};
input = rest;
out.resize(out.len().saturating_mul(2).min(limit), 0);
}
_ => {
out.truncate(written);
return Inflated::Damaged(out);
}
}
}
}
/// A stream's data with its filters undone: FlateDecode (with PNG predictors), or none.
fn decode(dict: &Dict, raw: &[u8], budget: &mut Budget) -> Option<Vec<u8>> {
if budget.exhausted {
@@ -547,20 +594,22 @@ fn decode(dict: &Dict, raw: &[u8], budget: &mut Budget) -> Option<Vec<u8>> {
Some(raw.to_vec())
}
[b"FlateDecode" | b"Fl"] => {
let inflated = match miniz_oxide::inflate::decompress_to_vec_zlib_with_limit(raw, limit) {
Ok(v) => v,
Err(e) if e.status == miniz_oxide::inflate::TINFLStatus::HasMoreOutput => {
let inflated = match inflate(raw, true, limit) {
Inflated::Done(v) => v,
Inflated::Exhausted => {
budget.exhausted = true;
return None;
}
// A damaged stream: what inflated before the damage, as other readers do.
Err(e) if !e.output.is_empty() => e.output,
Err(_) => match miniz_oxide::inflate::decompress_to_vec_with_limit(raw, limit) {
Ok(v) => v,
Err(e) => {
budget.exhausted |= e.status == miniz_oxide::inflate::TINFLStatus::HasMoreOutput;
Inflated::Damaged(v) if !v.is_empty() => v,
// Rejected before any output: retry as a bare deflate stream.
Inflated::Damaged(_) => match inflate(raw, false, limit) {
Inflated::Done(v) => v,
Inflated::Exhausted => {
budget.exhausted = true;
return None;
}
Inflated::Damaged(_) => return None,
},
};
unpredict(&inflated, parms, limit)
+58
View File
@@ -418,3 +418,61 @@ fn large_inherited_resources_are_written_once_for_every_copy() {
assert!(updated.len() < pdf.len() + (4 << 20), "{} bytes", updated.len());
assert_eq!(read(&updated).unwrap().sheets.len(), 3 * pages);
}
fn flate_dict() -> Dict {
vec![(b"Filter".to_vec(), Obj::Name(b"FlateDecode".to_vec()))]
}
fn varied(len: usize) -> Vec<u8> {
(0..len).map(|i| u8::try_from(i * i % 251).unwrap_or(0)).collect()
}
#[test]
fn a_valid_flate_stream_round_trips() {
let raw = varied(4000);
let packed = miniz_oxide::deflate::compress_to_vec_zlib(&raw, 6);
let mut budget = Budget::default();
assert_eq!(decode(&flate_dict(), &packed, &mut budget), Some(raw.clone()));
assert_eq!(budget.decoded, raw.len());
assert!(!budget.exhausted);
}
#[test]
fn a_truncated_flate_stream_yields_only_the_bytes_it_inflated() {
let raw = varied(4000);
let packed = miniz_oxide::deflate::compress_to_vec_zlib(&raw, 6);
let cut = packed.get(..packed.len() * 3 / 4).unwrap_or_default();
let mut budget = Budget::default();
let got = decode(&flate_dict(), cut, &mut budget).unwrap_or_default();
assert!(!got.is_empty() && got.len() < raw.len());
assert!(raw.starts_with(&got), "a prefix of the data, with no zero padding");
assert!(!budget.exhausted);
}
#[test]
fn a_stream_rejected_before_any_output_is_none_not_zeros() {
// A zlib header, then a reserved deflate block type: nothing is ever inflated, and the
// bare-deflate retry rejects it too.
let bad = [0x78, 0x9c, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff];
let mut budget = Budget::default();
assert!(decode(&flate_dict(), &bad, &mut budget).is_none());
assert_eq!(budget.decoded, 0);
assert!(!budget.exhausted);
}
#[test]
fn a_bare_deflate_stream_still_decodes_through_the_retry() {
let raw = varied(500);
let packed = miniz_oxide::deflate::compress_to_vec(&raw, 6);
let mut budget = Budget::default();
assert_eq!(decode(&flate_dict(), &packed, &mut budget), Some(raw));
}
#[test]
fn an_over_limit_flate_stream_sets_exhausted() {
let packed = miniz_oxide::deflate::compress_to_vec_zlib(&vec![7u8; 4096], 6);
let mut budget = Budget { decoded: MAX_DECODED - 100, ..Budget::default() };
assert!(decode(&flate_dict(), &packed, &mut budget).is_none());
assert!(budget.exhausted);
assert_eq!(budget.decoded, MAX_DECODED - 100);
}
+21
View File
@@ -273,6 +273,27 @@ fn group_dropped_on_an_iconic_column_joins_as_an_icon_group() {
assert!(first_column(&d.layout).iconic);
}
/// A group dropped on the window edge beside an iconic column at that edge docks in a new column
/// there: the preview is the edge line alone, without the column's outline.
#[test]
fn an_edge_drop_next_to_an_iconic_column_highlights_only_the_edge() {
let mut layout = docked(&[&[1], &[2]], true);
layout.floating.push(Floating::single(vec![5, 6], 0, [300.0, 200.0, 240.0, 200.0]));
let mut d = DockDriver::new(layout, Content);
let bar = hit(&d, Hit::DragBar(5)).center();
let target = pos2(2.0, 300.0);
d.hold(bar, target);
let column = Rect::from_min_max(pos2(0.0, 14.0), pos2(36.0, 600.0));
assert!(d.filled(Rect::from_x_y_ranges(0.0..=4.0, 0.0..=600.0)), "the edge line");
assert!(!d.outlined(column.shrink(1.0)), "the iconic column is not outlined");
d.release(target);
let Some(Node::Split { first, .. }) = &d.layout.root else { panic!("{:?}", d.layout.root) };
let Node::Dock { columns, .. } = first.as_ref() else { panic!("{first:?}") };
let panels: Vec<Vec<u32>> = columns.iter().map(|c| c.groups.iter().flat_map(|g| g.panels.clone()).collect()).collect();
assert_eq!(panels, vec![vec![5, 6], vec![1, 2]]);
assert!(!columns[0].iconic && columns[1].iconic);
}
/// Every kind of gesture source has a rect, and dragging from it does something.
#[test]
fn hit_rects_cover_every_gesture_source() {
+7 -3
View File
@@ -789,11 +789,15 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
}
/// The line of a drop that shows no result rect: on the window edge, into an iconic column,
/// or any dock drop with [`DockStyle::side_edge_zones`].
pub(super) fn paint_drop_line(&self, ctx: &Context, line: Rect) {
/// or any dock drop with [`DockStyle::side_edge_zones`]. A line inside an iconic column also
/// outlines the column, unless it is on the window edge (`outer`): a column at that edge holds
/// the line, but the drop docks beside the column, not into it.
pub(super) fn paint_drop_line(&self, ctx: &Context, line: Rect, outer: bool) {
if usable(line) {
let painter = self.preview_painter(ctx);
self.iconic.outline(&painter, line, self.style);
if !outer {
self.iconic.outline(&painter, line, self.style);
}
painter.rect_filled(line, 1.0, self.style.accent);
}
}
+3 -3
View File
@@ -599,12 +599,12 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
if self.style.side_edge_zones {
drop_preview(painter, proposal.preview.shrink(1.0), self.style.accent, filled);
} else if matches!(proposal.placement, Placement::Edge(_)) {
self.paint_drop_line(ctx, proposal.preview);
self.paint_drop_line(ctx, proposal.preview, true);
} else if beside {
let to = Target::At { anchor: proposal.anchor.clone(), placement: proposal.placement.clone() };
match self.result_rect(action(to), members) {
Some(rect) => tab_outline(painter, rect.shrink(1.0), self.style.accent),
None => self.paint_drop_line(ctx, proposal.preview),
None => self.paint_drop_line(ctx, proposal.preview, false),
}
} else {
tab_outline(painter, proposal.preview.shrink(1.0), self.style.accent);
@@ -621,7 +621,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
let rect = (!self.style.side_edge_zones && !drop.outer).then_some(action).and_then(|action| self.result_rect(action, members));
match rect {
Some(rect) => tab_outline(painter, rect.shrink(1.0), self.style.accent),
None => self.paint_drop_line(ctx, drop.line),
None => self.paint_drop_line(ctx, drop.line, drop.outer),
}
}
+15 -4
View File
@@ -91,13 +91,22 @@ impl StartupPrompt {
ui.label(RichText::new(self.app.name).size(15.0).strong().color(text_primary));
ui.label(RichText::new("UPDATE").size(10.0).monospace().color(accent_color));
});
let version_pill = format!("v{} → {}", self.running_version, self.release.version_label());
// ASCII only, like the rest of the card: an app whose fonts lack a glyph may panic
// instead of drawing a box, and this prompt shows up unasked.
let version_pill = format!("v{} to {}", self.running_version, self.release.version_label());
ui.label(RichText::new(version_pill).size(11.0).monospace().color(text_muted));
});
ui.with_layout(Layout::right_to_left(Align::Min), |ui| {
let x_btn = egui::Button::new(RichText::new("✕").size(12.0).color(text_muted)).frame(false);
if ui.add(x_btn).clicked() {
// Two strokes, not a "✕" glyph: none of the bundled faces has U+2715.
let (rect, close) = ui.allocate_exact_size(Vec2::splat(18.0), egui::Sense::click());
close.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, "Close"));
let color = if close.hovered() { text_primary } else { text_muted };
let (center, reach) = (rect.center(), 4.5);
let stroke = egui::Stroke::new(1.5, color);
ui.painter().line_segment([center + Vec2::new(-reach, -reach), center + Vec2::new(reach, reach)], stroke);
ui.painter().line_segment([center + Vec2::new(-reach, reach), center + Vec2::new(reach, -reach)], stroke);
if close.on_hover_cursor(egui::CursorIcon::PointingHand).clicked() {
response = StartupPromptResponse::Later;
}
});
@@ -304,7 +313,7 @@ mod tests {
for expected in [
"PhotoCraft",
"UPDATE",
"v0.1.0 → 0.2.0",
"v0.1.0 to 0.2.0",
"PhotoCraft 0.2.0 is available with the latest features and bug fixes.",
"What's new",
"DOWNLOAD",
@@ -313,6 +322,8 @@ mod tests {
assert!(painted.contains(expected), "missing {expected:?} in painted text {painted:?}");
}
assert!(!painted.contains("UPDATE AND RESTART"), "download branch must not offer the self-update button: {painted:?}");
// No glyph that a font might lack: apps that panic on a missing glyph would take the prompt down (#343).
assert!(painted.is_ascii(), "the prompt paints a non-ASCII character: {painted:?}");
let fills = rect_fills(&output);
let backdrop = fills.iter().any(|(rect, fill)| *fill == Color32::from_black_alpha(150) && rect.size() == screen().size());
assert!(backdrop, "modal backdrop missing: {fills:?}");
+2 -1
View File
@@ -1307,8 +1307,9 @@ pub mod macos {
Duration::from_secs(300),
)
};
// diskimages-helper can stay busy for a while on a shared runner; a minute of retries is still quick.
let mut attached = attach();
for wait in [1, 2, 4, 8] {
for wait in [1, 2, 4, 8, 16, 32] {
if attached.is_ok() {
break;
}