mirror of
https://github.com/storytold/craft-libs.git
synced 2026-10-31 07:39:23 +00:00
Merge remote-tracking branch 'upstream/main' into lang-2-ui
# Conflicts: # Cargo.lock
This commit is contained in:
commit
fdd6ad530e
9 files changed
+173
-29
No files matched your search
Generated
+6
-6
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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:?}");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user