mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-25 11:07:01 -06:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14f40c408c | ||
|
|
9ec3926793 | ||
|
|
79bf9a688c | ||
|
|
1330ce4b86 | ||
|
|
a0eeb341d2 |
Generated
+28
-28
@@ -857,7 +857,7 @@ checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f"
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft"
|
||||
version = "0.10.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"arboard",
|
||||
"chrono",
|
||||
@@ -878,7 +878,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-anim"
|
||||
version = "0.10.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"deckcraft-geom",
|
||||
"deckcraft-model",
|
||||
@@ -887,7 +887,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-av1"
|
||||
version = "0.10.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"deckcraft-bitstream",
|
||||
"rayon",
|
||||
@@ -896,14 +896,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-bitstream"
|
||||
version = "0.10.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"craft-media",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-cli"
|
||||
version = "0.10.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"deckcraft-engine",
|
||||
"deckcraft-mcp",
|
||||
@@ -913,14 +913,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-color"
|
||||
version = "0.10.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-engine"
|
||||
version = "0.10.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"deckcraft-anim",
|
||||
@@ -946,7 +946,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-fonts"
|
||||
version = "0.10.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"epaint_default_fonts",
|
||||
"harfrust 0.13.3",
|
||||
@@ -958,7 +958,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-format"
|
||||
version = "0.10.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"deckcraft-model",
|
||||
"serde",
|
||||
@@ -969,7 +969,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-geom"
|
||||
version = "0.10.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"kurbo",
|
||||
"serde",
|
||||
@@ -977,7 +977,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-h264"
|
||||
version = "0.10.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"deckcraft-bitstream",
|
||||
"rayon",
|
||||
@@ -986,7 +986,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-hevc"
|
||||
version = "0.10.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"deckcraft-bitstream",
|
||||
"rayon",
|
||||
@@ -995,21 +995,21 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-isobmff"
|
||||
version = "0.10.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"craft-media",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-matroska"
|
||||
version = "0.10.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"craft-media",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-mcp"
|
||||
version = "0.10.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"deckcraft-engine",
|
||||
"deckcraft-render",
|
||||
@@ -1019,7 +1019,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-media"
|
||||
version = "0.10.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"deckcraft-av1",
|
||||
"deckcraft-h264",
|
||||
@@ -1037,7 +1037,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-metafile"
|
||||
version = "0.10.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"image",
|
||||
"kurbo",
|
||||
@@ -1046,7 +1046,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-model"
|
||||
version = "0.10.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"deckcraft-color",
|
||||
"deckcraft-geom",
|
||||
@@ -1058,15 +1058,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-ogg"
|
||||
version = "0.10.0"
|
||||
version = "0.11.1"
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-opus"
|
||||
version = "0.10.0"
|
||||
version = "0.11.1"
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-pdf"
|
||||
version = "0.10.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"craft-pdf",
|
||||
"deckcraft-fonts",
|
||||
@@ -1084,7 +1084,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-pptx"
|
||||
version = "0.10.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"deckcraft-color",
|
||||
"deckcraft-engine",
|
||||
@@ -1100,7 +1100,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-render"
|
||||
version = "0.10.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"deckcraft-anim",
|
||||
"deckcraft-color",
|
||||
@@ -1119,7 +1119,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-text"
|
||||
version = "0.10.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"deckcraft-color",
|
||||
@@ -1133,7 +1133,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-ui-egui"
|
||||
version = "0.10.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"deckcraft-anim",
|
||||
"deckcraft-color",
|
||||
@@ -1156,7 +1156,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-vp9"
|
||||
version = "0.10.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"deckcraft-bitstream",
|
||||
"rayon",
|
||||
@@ -1165,7 +1165,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deckcraft-web"
|
||||
version = "0.10.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"deckcraft-engine",
|
||||
"deckcraft-ui-egui",
|
||||
@@ -5564,7 +5564,7 @@ checksum = "9440ea3e5aeabb0ac63af70daf835274065238cdd0cec83418f417eae38bacee"
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.10.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ members = ["crates/*", "apps/*", "xtask"]
|
||||
default-members = ["crates/*", "apps/deckcraft", "apps/deckcraft-cli"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.10.0"
|
||||
version = "0.11.1"
|
||||
edition = "2024"
|
||||
license = "MIT OR Apache-2.0"
|
||||
rust-version = "1.95"
|
||||
|
||||
@@ -515,6 +515,244 @@ pub fn preset(name: &str) -> Option<Rgba> {
|
||||
Some(Rgba::rgb(c.0, c.1, c.2))
|
||||
}
|
||||
|
||||
/// Every preset colour name a file may write (`ST_PresetColorVal`, with the later editions' aliases).
|
||||
pub const PRESET_NAMES: [&str; 190] = [
|
||||
"aliceBlue",
|
||||
"antiqueWhite",
|
||||
"aqua",
|
||||
"aquamarine",
|
||||
"azure",
|
||||
"beige",
|
||||
"bisque",
|
||||
"black",
|
||||
"blanchedAlmond",
|
||||
"blue",
|
||||
"blueViolet",
|
||||
"brown",
|
||||
"burlyWood",
|
||||
"cadetBlue",
|
||||
"chartreuse",
|
||||
"chocolate",
|
||||
"coral",
|
||||
"cornflowerBlue",
|
||||
"cornsilk",
|
||||
"crimson",
|
||||
"cyan",
|
||||
"darkBlue",
|
||||
"darkCyan",
|
||||
"darkGoldenrod",
|
||||
"darkGray",
|
||||
"darkGrey",
|
||||
"darkGreen",
|
||||
"darkKhaki",
|
||||
"darkMagenta",
|
||||
"darkOliveGreen",
|
||||
"darkOrange",
|
||||
"darkOrchid",
|
||||
"darkRed",
|
||||
"darkSalmon",
|
||||
"darkSeaGreen",
|
||||
"darkSlateBlue",
|
||||
"darkSlateGray",
|
||||
"darkSlateGrey",
|
||||
"darkTurquoise",
|
||||
"darkViolet",
|
||||
"dkBlue",
|
||||
"dkCyan",
|
||||
"dkGoldenrod",
|
||||
"dkGray",
|
||||
"dkGrey",
|
||||
"dkGreen",
|
||||
"dkKhaki",
|
||||
"dkMagenta",
|
||||
"dkOliveGreen",
|
||||
"dkOrange",
|
||||
"dkOrchid",
|
||||
"dkRed",
|
||||
"dkSalmon",
|
||||
"dkSeaGreen",
|
||||
"dkSlateBlue",
|
||||
"dkSlateGray",
|
||||
"dkSlateGrey",
|
||||
"dkTurquoise",
|
||||
"dkViolet",
|
||||
"deepPink",
|
||||
"deepSkyBlue",
|
||||
"dimGray",
|
||||
"dimGrey",
|
||||
"dodgerBlue",
|
||||
"firebrick",
|
||||
"floralWhite",
|
||||
"forestGreen",
|
||||
"fuchsia",
|
||||
"gainsboro",
|
||||
"ghostWhite",
|
||||
"gold",
|
||||
"goldenrod",
|
||||
"gray",
|
||||
"grey",
|
||||
"green",
|
||||
"greenYellow",
|
||||
"honeydew",
|
||||
"hotPink",
|
||||
"indianRed",
|
||||
"indigo",
|
||||
"ivory",
|
||||
"khaki",
|
||||
"lavender",
|
||||
"lavenderBlush",
|
||||
"lawnGreen",
|
||||
"lemonChiffon",
|
||||
"lightBlue",
|
||||
"lightCoral",
|
||||
"lightCyan",
|
||||
"lightGoldenrodYellow",
|
||||
"lightGray",
|
||||
"lightGrey",
|
||||
"lightGreen",
|
||||
"lightPink",
|
||||
"lightSalmon",
|
||||
"lightSeaGreen",
|
||||
"lightSkyBlue",
|
||||
"lightSlateGray",
|
||||
"lightSlateGrey",
|
||||
"lightSteelBlue",
|
||||
"lightYellow",
|
||||
"ltBlue",
|
||||
"ltCoral",
|
||||
"ltCyan",
|
||||
"ltGoldenrodYellow",
|
||||
"ltGray",
|
||||
"ltGrey",
|
||||
"ltGreen",
|
||||
"ltPink",
|
||||
"ltSalmon",
|
||||
"ltSeaGreen",
|
||||
"ltSkyBlue",
|
||||
"ltSlateGray",
|
||||
"ltSlateGrey",
|
||||
"ltSteelBlue",
|
||||
"ltYellow",
|
||||
"lime",
|
||||
"limeGreen",
|
||||
"linen",
|
||||
"magenta",
|
||||
"maroon",
|
||||
"medAquamarine",
|
||||
"medBlue",
|
||||
"medOrchid",
|
||||
"medPurple",
|
||||
"medSeaGreen",
|
||||
"medSlateBlue",
|
||||
"medSpringGreen",
|
||||
"medTurquoise",
|
||||
"medVioletRed",
|
||||
"mediumAquamarine",
|
||||
"mediumBlue",
|
||||
"mediumOrchid",
|
||||
"mediumPurple",
|
||||
"mediumSeaGreen",
|
||||
"mediumSlateBlue",
|
||||
"mediumSpringGreen",
|
||||
"mediumTurquoise",
|
||||
"mediumVioletRed",
|
||||
"midnightBlue",
|
||||
"mintCream",
|
||||
"mistyRose",
|
||||
"moccasin",
|
||||
"navajoWhite",
|
||||
"navy",
|
||||
"oldLace",
|
||||
"olive",
|
||||
"oliveDrab",
|
||||
"orange",
|
||||
"orangeRed",
|
||||
"orchid",
|
||||
"paleGoldenrod",
|
||||
"paleGreen",
|
||||
"paleTurquoise",
|
||||
"paleVioletRed",
|
||||
"papayaWhip",
|
||||
"peachPuff",
|
||||
"peru",
|
||||
"pink",
|
||||
"plum",
|
||||
"powderBlue",
|
||||
"purple",
|
||||
"red",
|
||||
"rosyBrown",
|
||||
"royalBlue",
|
||||
"saddleBrown",
|
||||
"salmon",
|
||||
"sandyBrown",
|
||||
"seaGreen",
|
||||
"seaShell",
|
||||
"sienna",
|
||||
"silver",
|
||||
"skyBlue",
|
||||
"slateBlue",
|
||||
"slateGray",
|
||||
"slateGrey",
|
||||
"snow",
|
||||
"springGreen",
|
||||
"steelBlue",
|
||||
"tan",
|
||||
"teal",
|
||||
"thistle",
|
||||
"tomato",
|
||||
"turquoise",
|
||||
"violet",
|
||||
"wheat",
|
||||
"white",
|
||||
"whiteSmoke",
|
||||
"yellow",
|
||||
"yellowGreen",
|
||||
];
|
||||
|
||||
/// Every system colour name a file may write (`ST_SystemColorVal`).
|
||||
pub const SYSTEM_NAMES: [&str; 30] = [
|
||||
"scrollBar",
|
||||
"background",
|
||||
"activeCaption",
|
||||
"inactiveCaption",
|
||||
"menu",
|
||||
"window",
|
||||
"windowFrame",
|
||||
"menuText",
|
||||
"windowText",
|
||||
"captionText",
|
||||
"activeBorder",
|
||||
"inactiveBorder",
|
||||
"appWorkspace",
|
||||
"highlight",
|
||||
"highlightText",
|
||||
"btnFace",
|
||||
"btnShadow",
|
||||
"grayText",
|
||||
"btnText",
|
||||
"inactiveCaptionText",
|
||||
"btnHighlight",
|
||||
"3dDkShadow",
|
||||
"3dLight",
|
||||
"infoText",
|
||||
"infoBk",
|
||||
"hotLight",
|
||||
"gradientActiveCaption",
|
||||
"gradientInactiveCaption",
|
||||
"menuHighlight",
|
||||
"menuBar",
|
||||
];
|
||||
|
||||
/// Whether `name` is a preset colour name a file may write.
|
||||
pub fn is_preset_name(name: &str) -> bool {
|
||||
PRESET_NAMES.contains(&name)
|
||||
}
|
||||
|
||||
/// Whether `name` is a system colour name a file may write.
|
||||
pub fn is_system_name(name: &str) -> bool {
|
||||
SYSTEM_NAMES.contains(&name)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -245,7 +245,11 @@ fn bring_media(cmd: &str, doc: &mut deckcraft_model::Presentation, media: &[deck
|
||||
let mut map = HashMap::new();
|
||||
for m in media {
|
||||
match doc.media(m.id) {
|
||||
None => doc.media.push(m.clone()),
|
||||
None => {
|
||||
// Keeps the foreign id, so later ids must start past it.
|
||||
doc.media.push(m.clone());
|
||||
doc.next_id = doc.next_id.max(m.id.0);
|
||||
}
|
||||
Some(x) if x.data == m.data => {}
|
||||
Some(_) => {
|
||||
let id = doc.add_media(&m.name, &m.content_type, m.data.as_ref().clone()).ok_or_else(|| bad(cmd, NO_ID_SPACE))?;
|
||||
|
||||
@@ -394,8 +394,14 @@ pub(crate) fn color_value(v: &Value) -> Option<deckcraft_model::ColorRef> {
|
||||
let mut c = if let Some(s) = o.get("scheme").and_then(Value::as_str) {
|
||||
ColorRef::scheme(deckcraft_color::SchemeSlot::from_xml(s)?)
|
||||
} else if let Some(name) = o.get("preset").and_then(Value::as_str) {
|
||||
if !deckcraft_color::is_preset_name(name) {
|
||||
return None;
|
||||
}
|
||||
ColorRef { base: deckcraft_model::ColorBase::Preset { name: name.to_string() }, mods: vec![] }
|
||||
} else if let Some(name) = o.get("system").and_then(Value::as_str) {
|
||||
if !deckcraft_color::is_system_name(name) {
|
||||
return None;
|
||||
}
|
||||
let last = deckcraft_color::Rgba::from_hex(o.get("last").and_then(Value::as_str)?)?;
|
||||
ColorRef { base: deckcraft_model::ColorBase::System { name: name.to_string(), last }, mods: vec![] }
|
||||
} else {
|
||||
@@ -657,5 +663,11 @@ mod tests {
|
||||
let system = color_value(&json!({"system": "windowText", "last": "#123456"}));
|
||||
assert_eq!(system, Some(ColorRef { base: ColorBase::System { name: "windowText".into(), last: Rgba::rgb(0x12, 0x34, 0x56) }, mods: vec![] }));
|
||||
assert_eq!(color_value(&json!({"system": "windowText"})), None, "a system color needs its last value");
|
||||
assert_eq!(color_value(&json!({"preset": "notAColor"})), None);
|
||||
assert_eq!(color_value(&json!({"system": "notAColor", "last": "#000000"})), None);
|
||||
for name in ["aliceBlue", "darkBlue", "ltSlateGrey"] {
|
||||
assert!(color_value(&json!({"preset": name})).is_some(), "{name}");
|
||||
}
|
||||
assert!(color_value(&json!({"system": "3dDkShadow", "last": "#000000"})).is_some());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,7 +286,11 @@ pub fn adopt(doc: &mut Presentation, rev: &Presentation, slide: &Slide, replacin
|
||||
let Some(m) = rev.media(id) else { continue };
|
||||
match doc.media(id) {
|
||||
Some(x) if x.data == m.data => {}
|
||||
None => doc.media.push(m.clone()),
|
||||
None => {
|
||||
// Keeps the revision's id, so later ids must start past it.
|
||||
doc.media.push(m.clone());
|
||||
doc.next_id = doc.next_id.max(id.0);
|
||||
}
|
||||
Some(_) => {
|
||||
let new = doc
|
||||
.add_media(&m.name, &m.content_type, m.data.as_ref().clone())
|
||||
|
||||
@@ -947,6 +947,34 @@ fn dragging_across_cells_selects_a_range_of_cells() {
|
||||
assert_eq!(cmd::table::cell_block(&t, (1, 1), (2, 2)), ((0, 0), (2, 2)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pressing_a_table_outside_its_cells_with_a_range_selected_moves_the_table() {
|
||||
let mut s = session();
|
||||
let id = table_at(&mut s, &[100.0, 100.0], json!([["a", "b"], ["c", "d"]]));
|
||||
let drag = |s: &mut Session, kind, x: f64, y: f64| s.pointer(tev(kind, x, y)).unwrap();
|
||||
s.execute("text.edit", &json!({"id": id, "cell": [0, 0]})).unwrap();
|
||||
drag(&mut s, PointerKind::Down, 150.0, 120.0);
|
||||
drag(&mut s, PointerKind::Drag, 250.0, 160.0);
|
||||
drag(&mut s, PointerKind::Up, 250.0, 160.0);
|
||||
assert_eq!(s.doc().unwrap().selection.cells, Some((id, (0, 0), (1, 1))));
|
||||
// Just left of the table's edge, where a press still hits the table but no cell.
|
||||
drag(&mut s, PointerKind::Down, 98.0, 120.0);
|
||||
assert_eq!(s.doc().unwrap().selection.cells, None, "the range ends");
|
||||
drag(&mut s, PointerKind::Drag, 148.0, 170.0);
|
||||
drag(&mut s, PointerKind::Up, 148.0, 170.0);
|
||||
let st = s.doc().unwrap();
|
||||
assert!(st.selection.text.is_none());
|
||||
assert_eq!(st.selection.shapes, vec![id]);
|
||||
let x = st.shape(id).unwrap().xfrm.unwrap();
|
||||
assert!((x.x - 150.0).abs() < 10.0 && (x.y - 150.0).abs() < 10.0, "the table moves: {x:?}");
|
||||
// A click (no drag) there keeps the table selected and puts no caret in the nearest cell.
|
||||
let (x0, y0) = (x.x - 2.0, x.y + 20.0);
|
||||
drag(&mut s, PointerKind::Down, x0, y0);
|
||||
drag(&mut s, PointerKind::Up, x0, y0);
|
||||
let st = s.doc().unwrap();
|
||||
assert_eq!((st.selection.shapes.clone(), st.selection.text.is_none()), (vec![id], true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_click_in_a_selected_table_enters_the_clicked_cell() {
|
||||
let mut s = session();
|
||||
@@ -1970,6 +1998,45 @@ mod copy_remap {
|
||||
assert_eq!(img.get_pixel(img.width() / 2, img.height() / 2).0, [255, 0, 0, 255]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pictures_inserted_after_a_paste_from_another_deck_get_media_ids_of_their_own() {
|
||||
let mut a = Session::with_new();
|
||||
let mut b = Session::with_new();
|
||||
let (pa, ..) = populate(&mut a, b"deck a picture");
|
||||
// Deck a's picture uses a media id above its shape ids that deck b has not handed out yet.
|
||||
{
|
||||
let doc = Arc::make_mut(&mut a.doc_mut().unwrap().doc);
|
||||
let high = MediaId(doc.next_id + 20);
|
||||
doc.next_id = high.0;
|
||||
doc.media[0].id = high;
|
||||
let slide = Arc::make_mut(&mut doc.slides[0]);
|
||||
let Some(ShapeKind::Picture { fill }) = slide.shapes.iter_mut().find(|x| x.id == pa).map(|x| &mut x.kind) else { panic!() };
|
||||
fill.media = high;
|
||||
}
|
||||
assert_eq!(picture_bytes(&a, &shape(&a, 0, pa)), b"deck a picture");
|
||||
select(&mut a, &[pa]);
|
||||
a.execute("edit.copy", &json!({})).unwrap();
|
||||
b.clipboard = a.clipboard.clone();
|
||||
b.doc_mut().unwrap().selection.shapes.clear();
|
||||
let r = b.execute("edit.paste", &json!({})).unwrap();
|
||||
let pasted = ShapeId(r["ids"][0].as_u64().unwrap() as u32);
|
||||
let mut inserted = vec![];
|
||||
for i in 0..12u8 {
|
||||
let png = solid_png([i, 0, 0]);
|
||||
let r = b.execute("insert.picture", &json!({"data": cmd::base64_encode(&png), "name": "a.png"})).unwrap();
|
||||
inserted.push((ShapeId(r["id"].as_u64().unwrap() as u32), png));
|
||||
}
|
||||
assert_eq!(picture_bytes(&b, &shape(&b, 0, pasted)), b"deck a picture");
|
||||
for (id, png) in inserted {
|
||||
assert_eq!(picture_bytes(&b, &shape(&b, 0, id)), png);
|
||||
}
|
||||
let d = b.doc().unwrap();
|
||||
let mut ids: Vec<_> = d.doc.media.iter().map(|m| m.id).collect();
|
||||
ids.sort();
|
||||
ids.dedup();
|
||||
assert_eq!(ids.len(), d.doc.media.len(), "media ids are unique");
|
||||
}
|
||||
|
||||
/// Slide 0 of `s` gets a table whose first cell, background and second cell's picture bullet
|
||||
/// each use a picture of their own, and a transition sound. `tag` starts the bytes of each.
|
||||
/// Returns the table.
|
||||
|
||||
@@ -705,10 +705,13 @@ impl Session {
|
||||
}
|
||||
}
|
||||
Some(Hit::Shape { id, .. })
|
||||
if !(mods.shift || mods.cmd) && st.selection.cells.is_some_and(|c| c.0 == id && st.selection.shapes == [id]) =>
|
||||
if !(mods.shift || mods.cmd)
|
||||
&& st.selection.cells.is_some_and(|c| c.0 == id && st.selection.shapes == [id])
|
||||
&& in_table_cell(st, id, p) =>
|
||||
{
|
||||
// A press in a table whose cells are selected starts again in the cell under the
|
||||
// pointer: a click puts the caret there and a drag selects another range.
|
||||
// pointer: a click puts the caret there and a drag selects another range. A press
|
||||
// on the table outside its cells selects and moves the table like any shape.
|
||||
let before = self.text_selection();
|
||||
if self.edit_text_at(id, p)? {
|
||||
self.select(|_, sel| sel.cells = None)?;
|
||||
@@ -736,15 +739,17 @@ impl Session {
|
||||
let origs: Vec<(ShapeId, Xfrm)> =
|
||||
ids.iter().filter_map(|i| st.shape(*i).filter(|s| !s.locked).map(|s| (*i, xfrm_of(&st.doc, &st.selection, s)))).collect();
|
||||
// A click (no drag) on text puts the caret there; unselected text boxes and
|
||||
// placeholders take the caret straight away like PowerPoint. A click in a selected
|
||||
// table puts the caret in the clicked cell.
|
||||
// placeholders take the caret straight away like PowerPoint. A click in a cell of a
|
||||
// selected table puts the caret there; a click on its grab band keeps the table selected.
|
||||
let table = st.shape(id).is_some_and(|s| matches!(s.kind, ShapeKind::Table(_)));
|
||||
// A click on the frame band of a text box selects the box whole, as PowerPoint
|
||||
// does, so it can be moved, duplicated or deleted; only the inside takes the caret.
|
||||
let on_frame = text
|
||||
&& is_text_first(st, id)
|
||||
&& st.shape(id).is_some_and(|s| edit_zone(&xfrm_of(&st.doc, &st.selection, s), p, tol) == EditZone::Frame);
|
||||
let enter_text = (!toggle && !on_frame && ((text && (selected || is_text_first(st, id))) || (table && selected))).then_some((id, p));
|
||||
let enter_text =
|
||||
(!toggle && !on_frame && ((text && (selected || is_text_first(st, id))) || (table && selected && in_table_cell(st, id, p))))
|
||||
.then_some((id, p));
|
||||
let ids2 = ids.clone();
|
||||
self.select(|_, sel| {
|
||||
sel.shapes = ids2;
|
||||
@@ -1990,6 +1995,15 @@ pub fn resize_box(o: &Xfrm, h: u8, d: Vec2, keep_aspect: bool, from_center: bool
|
||||
Xfrm { x: c.x - nw / 2.0, y: c.y - nh / 2.0, w: nw, h: nh, rot: o.rot, flip_h: fh, flip_v: fv }
|
||||
}
|
||||
|
||||
/// Whether `p` lies in a cell of table `id`, not in the grab band around it (`cell_at` would take
|
||||
/// the nearest cell).
|
||||
fn in_table_cell(st: &crate::DocState, id: ShapeId, p: Point) -> bool {
|
||||
st.shape(id).and_then(|sh| cmd::table::table_frame(st, sh)).is_some_and(|f| {
|
||||
let q = f.local(p);
|
||||
(0.0..=f.width()).contains(&q.x) && (0.0..=f.height()).contains(&q.y) && f.cell_at(q).is_some()
|
||||
})
|
||||
}
|
||||
|
||||
fn hit_shape(st: &crate::DocState, sh: &Shape, p: Point, tol: f64) -> Option<Hit> {
|
||||
let x = xfrm_of(&st.doc, &st.selection, sh);
|
||||
let inv = x.affine().inverse();
|
||||
|
||||
@@ -36,9 +36,10 @@ pub fn color(w: &mut W, c: &ColorRef) {
|
||||
let (tag, a) = match &c.base {
|
||||
ColorBase::Rgb { rgb } => ("a:srgbClr", A::new().a("val", rgb.hex())),
|
||||
ColorBase::Scheme { slot } => ("a:schemeClr", A::new().a("val", slot.xml_name())),
|
||||
ColorBase::Preset { name } => ("a:prstClr", A::new().a("val", if name.is_empty() { "black" } else { name })),
|
||||
// The reader keeps any name a file has; only names the schema allows are written.
|
||||
ColorBase::Preset { name } => ("a:prstClr", A::new().a("val", if deckcraft_color::is_preset_name(name) { name } else { "black" })),
|
||||
ColorBase::System { name, last } => {
|
||||
("a:sysClr", A::new().a("val", if name.is_empty() { "windowText" } else { name }).a("lastClr", last.hex()))
|
||||
("a:sysClr", A::new().a("val", if deckcraft_color::is_system_name(name) { name } else { "windowText" }).a("lastClr", last.hex()))
|
||||
}
|
||||
};
|
||||
let rgb_alpha = match &c.base {
|
||||
@@ -788,3 +789,23 @@ pub fn paragraph(w: &mut W, x: &mut Exp, o: &mut Out, p: &Paragraph, math: bool,
|
||||
pub fn has_math(tb: &TextBody) -> bool {
|
||||
tb.paragraphs.iter().flat_map(|p| &p.runs).any(|r| matches!(&r.kind, RunKind::Math { omml } if !omml.is_empty()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use deckcraft_color::Rgba;
|
||||
|
||||
#[test]
|
||||
fn unknown_preset_and_system_names_save_as_black_and_window_text() {
|
||||
let write = |base: ColorBase| {
|
||||
let mut w = W::frag();
|
||||
color(&mut w, &ColorRef { base, mods: vec![] });
|
||||
w.s
|
||||
};
|
||||
assert_eq!(write(ColorBase::Preset { name: "notAColor".into() }), r#"<a:prstClr val="black"/>"#);
|
||||
assert_eq!(write(ColorBase::Preset { name: "darkBlue".into() }), r#"<a:prstClr val="darkBlue"/>"#);
|
||||
let last = Rgba::rgb(0x12, 0x34, 0x56);
|
||||
assert_eq!(write(ColorBase::System { name: "notAColor".into(), last }), r#"<a:sysClr val="windowText" lastClr="123456"/>"#);
|
||||
assert_eq!(write(ColorBase::System { name: "3dLight".into(), last }), r#"<a:sysClr val="3dLight" lastClr="123456"/>"#);
|
||||
}
|
||||
}
|
||||
|
||||
+18
-3
@@ -61,6 +61,12 @@ revision history.
|
||||
- **Impact:** long-session crash rate is sampled in CI; RSS is not measured directly.
|
||||
- **Estimate:** 1–2 h (RSS sampling per platform).
|
||||
|
||||
### A control-server timing test may fail under load
|
||||
- **Evidence:** `silent_refused_peers_do_not_delay_the_next_refusal` in
|
||||
`apps/deckcraft/src/control_server.rs` asserts that a refusal arrives in under 1 s, so it can
|
||||
fail on a busy machine although the server behaves correctly.
|
||||
- **Estimate:** under 1 h (a looser bound or a logical clock).
|
||||
|
||||
## File formats
|
||||
|
||||
Detail in [file-format-parity.md](file-format-parity.md).
|
||||
@@ -260,9 +266,17 @@ and starts at that effect. The pane has an advanced timeline with draggable bars
|
||||
`review.thesaurus` looks up the selected word (checklist row partial). AutoCorrect has
|
||||
PowerPoint's options (two initial capitals, sentence and day capitals, caps lock, fractions, an
|
||||
editable replace list), and AutoFormat superscripts ordinals (1st) and starts bulleted and
|
||||
numbered lists (`* `, `1. `, Backspace puts the typed text back) as you type; no undo of a
|
||||
single correction.
|
||||
- **Estimate:** 1–2 h (bundle openly licensed dictionaries).
|
||||
numbered lists (`* `, `1. `) as you type; no undo of a single correction. Automatic lists start
|
||||
only while Replace text as you type is on, where PowerPoint has a separate AutoFormat option,
|
||||
and Backspace right after one puts the typed lead back, which PowerPoint does not.
|
||||
- **Estimate:** 1–2 h (bundle openly licensed dictionaries; under 1 h for the AutoFormat option).
|
||||
|
||||
### Copying text and cell ranges
|
||||
- **Evidence:** a copied text range carries no media, so a picture bullet in it is lost when
|
||||
pasted into another deck; the writer saves a bullet whose picture is missing as `•`. A copied
|
||||
range of table cells is tab-separated text, not a table, so pasting it makes text rather than
|
||||
cells.
|
||||
- **Estimate:** 2–4 h.
|
||||
|
||||
### Equations
|
||||
- **Evidence:** checklist row missing; PR #55 (OMML editor, +10.5k lines) is open.
|
||||
@@ -357,6 +371,7 @@ Detail in [localization-parity.md](localization-parity.md).
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-25 | trivial | New entries: copying text and cell ranges (no media, no table), a control-server timing test that may fail under load; Proofing notes that automatic lists follow Replace text as you type and that Backspace puts the typed lead back |
|
||||
| 2026-10-24 | trivial | AutoFormat starts bulleted and numbered lists as you type (`* `, `-- `, `> `, `1. `, `a) ` and the rest), with an AutoCorrect option to turn it off |
|
||||
| 2026-10-22 | minor | Animation Pane ▸ Play From starts the preview at the selected effect; gap 10 keeps the effect gallery (7–13 h) |
|
||||
| 2026-10-24 | trivial | Columns dialog (number and spacing of text columns); dialog count 41 → 42 |
|
||||
|
||||
@@ -24,7 +24,9 @@ KEYCHAIN="$TMP_DIR/deckcraft-signing.keychain-db"
|
||||
CERT="$TMP_DIR/deckcraft-signing.p12"
|
||||
KC_PASS="${KEYCHAIN_PASSWORD:-$(openssl rand -hex 24)}"
|
||||
|
||||
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||
# Remove only the decoded certificate: the keychain lives in the same directory and codesign
|
||||
# needs it until the workflow's "Remove temporary keychain" step deletes it.
|
||||
trap 'rm -f "${CERT:-}"' EXIT
|
||||
printf '%s' "$APPLE_CERTIFICATE" | base64 --decode >"$CERT"
|
||||
security create-keychain -p "$KC_PASS" "$KEYCHAIN"
|
||||
security set-keychain-settings -lut 21600 "$KEYCHAIN"
|
||||
|
||||
Reference in New Issue
Block a user