mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-31 05:56:55 -06:00
@@ -164,6 +164,7 @@ app's JSON control channel (`deckcraft --control PORT`).
|
||||
| 2026-10-20 | Chart number formats with explicit empty positive, negative or zero sections hide the corresponding labels, including all values with three semicolons; a blank whole format still uses General. Synthetic formatter regressions |
|
||||
| 2026-10-22 | Chart bars and their legend keys draw the series' own pattern fill (`a:pattFill`, now read and written) and draw nothing for `a:noFill`, with the series outline where it has one, as PowerPoint for Mac does (synthetic probe); they took the palette's colour before. Lines, areas and markers use a pattern's foreground colour |
|
||||
| 2026-10-24 | Format Shape and Format Background ▸ Pattern fill: a gallery of the 48 preset patterns PowerPoint offers, drawn in the current colors, with Foreground and Background color menus. Format Shape could only set 25 % in accent 1 on background 1, and Format Background listed 10 presets by their XML names |
|
||||
| 2026-10-25 | Format Shape ▸ Fill: Slide background fill shows the slide background through the shape (`shape.fill {background: true}`, saved as `useBgFill`). A shape opened from a file with this fill showed it as Automatic, and the pane offered no way to set it |
|
||||
| 2026-10-21 | Reuse Slides: pick which slides of the other presentation to insert from a list (`slide.reuseList`, #1103) |
|
||||
| 2026-10-21 | Slide list and slide sorter: Shift+click selects a range of slides and Ctrl+Shift+click adds it to the selection (`slide.selectRange`, #1105) |
|
||||
| 2026-10-18 | Native chart sidecar bytes move from JSON arrays into referenced ZIP entries, with shared media byte/item limits and bounded reads; older inline sidecars still load. Synthetic format regressions cover large workbooks and malformed members |
|
||||
|
||||
@@ -103,7 +103,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Shape Fill",
|
||||
["Shape Format", "Shape Styles"],
|
||||
None,
|
||||
"{color? | none?: true | gradient?: {stops: [[pos, color]], angle?, kind?: linear|radial} | picture?: base64 | pattern?: {preset, fg, bg} | transparency?: 0..1 | reset?: true, ids?}; pictureProps?: {tile?: bool, offsetX?, offsetY?: pt, scaleX?, scaleY?: 0..10, align?: tl|t|tr|l|ctr|r|bl|b|br, mirror?: none|x|y|xy, stretch?: [l, t, r, b] fractions, rotateWithShape?: bool} adjusts a picture fill; a group also formats its members",
|
||||
"{color? | none?: true | background?: true (the slide background shows through; shapes and text boxes only) | gradient?: {stops: [[pos, color]], angle?, kind?: linear|radial} | picture?: base64 | pattern?: {preset, fg, bg} | transparency?: 0..1 | reset?: true, ids?}; pictureProps?: {tile?: bool, offsetX?, offsetY?: pt, scaleX?, scaleY?: 0..10, align?: tl|t|tr|l|ctr|r|bl|b|br, mirror?: none|x|y|xy, stretch?: [l, t, r, b] fractions, rotateWithShape?: bool} adjusts a picture fill; a group also formats its members",
|
||||
has_selection,
|
||||
fill
|
||||
),
|
||||
@@ -261,6 +261,9 @@ fn requested_fill(s: &mut Session, p: &Value) -> Result<Option<Option<Fill>>> {
|
||||
if bool_or(p, "none", false) {
|
||||
return Ok(Some(Some(Fill::None)));
|
||||
}
|
||||
if bool_or(p, "background", false) {
|
||||
return Ok(Some(Some(Fill::Background)));
|
||||
}
|
||||
if let Some(c) = color_param(p, "color") {
|
||||
return Ok(Some(Some(Fill::solid(c))));
|
||||
}
|
||||
@@ -503,6 +506,103 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn background_fill_shows_the_slide_background_and_survives_a_save() {
|
||||
let mut s = Session::with_new();
|
||||
s.execute("slide.new", &json!({"layout": "blank"})).unwrap();
|
||||
s.execute("design.background", &json!({"color": "#0000c8"})).unwrap();
|
||||
let id = ShapeId(s.execute("shape.insert", &json!({"preset": "rect", "rect": [100, 100, 200, 100]})).unwrap()["id"].as_u64().unwrap() as u32);
|
||||
let inside = |s: &mut Session| s.execute("slide.colorAt", &json!({"x": 200, "y": 150})).unwrap()["color"].clone();
|
||||
assert_ne!(inside(&mut s), "0000C8", "a new shape takes the theme's fill");
|
||||
s.execute("shape.fill", &json!({"background": true})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().shape(id).unwrap().fill, Some(Fill::Background));
|
||||
assert_eq!(inside(&mut s), "0000C8", "the slide background shows through the shape");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().shape(id).unwrap().fill, None, "one undo step returns to the theme's fill");
|
||||
s.execute("edit.redo", &json!({})).unwrap();
|
||||
// Transparency changes a solid or picture fill only.
|
||||
s.execute("shape.fill", &json!({"transparency": 0.5})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().shape(id).unwrap().fill, Some(Fill::Background));
|
||||
|
||||
let bytes = deckcraft_pptx::export(&s.doc().unwrap().doc).unwrap();
|
||||
let back = deckcraft_pptx::import(&bytes).unwrap();
|
||||
let filled = back.slides.iter().flat_map(|sl| sl.shapes.iter()).filter(|sh| sh.fill == Some(Fill::Background)).count();
|
||||
assert_eq!(filled, 1, "the shape keeps the background fill after a save and reopen");
|
||||
}
|
||||
|
||||
/// A picture cannot save a slide background fill, so `background` leaves its fill alone.
|
||||
#[test]
|
||||
fn background_fill_skips_pictures() {
|
||||
let mut s = Session::with_new();
|
||||
s.execute("slide.new", &json!({"layout": "blank"})).unwrap();
|
||||
let shape = s.execute("shape.insert", &json!({"preset": "rect", "rect": [100, 100, 200, 100]})).unwrap()["id"].as_u64().unwrap();
|
||||
let png = deckcraft_render::Image { width: 2, height: 2, pixels: [255, 0, 0, 255].repeat(4) }.to_png();
|
||||
let pic = s
|
||||
.execute("insert.picture", &json!({"data": crate::cmd::base64_encode(&png), "name": "red.png", "rect": [400, 100, 100, 100]}))
|
||||
.unwrap()["id"]
|
||||
.as_u64()
|
||||
.unwrap();
|
||||
let fill = |s: &Session, id: u64| s.doc().unwrap().shape(ShapeId(id as u32)).unwrap().fill.clone();
|
||||
let err = s.execute("shape.fill", &json!({"background": true, "ids": [pic]})).unwrap_err();
|
||||
assert!(err.to_string().contains("shapes and text boxes"), "{err}");
|
||||
assert_eq!(fill(&s, pic), None);
|
||||
|
||||
s.execute("edit.select", &json!({"ids": [shape, pic]})).unwrap();
|
||||
let group = s.execute("arrange.group", &json!({})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("shape.fill", &json!({"color": "#ff0000"})).unwrap();
|
||||
s.execute("shape.fill", &json!({"background": true})).unwrap();
|
||||
assert_eq!(fill(&s, shape), Some(Fill::Background));
|
||||
assert!(matches!(fill(&s, pic), Some(Fill::Solid { .. })), "the picture keeps its fill");
|
||||
assert_eq!(fill(&s, group), None, "the group's own fill is cleared");
|
||||
let bytes = deckcraft_pptx::export(&s.doc().unwrap().doc).unwrap();
|
||||
let back = deckcraft_pptx::import(&bytes).unwrap();
|
||||
let mut filled = 0;
|
||||
for slide in &back.slides {
|
||||
deckcraft_model::walk(&slide.shapes, &mut |sh, _| filled += usize::from(sh.fill == Some(Fill::Background)));
|
||||
}
|
||||
assert_eq!(filled, 1, "the shape keeps the background fill after a save and reopen");
|
||||
}
|
||||
|
||||
/// A picture filled with its group's fill keeps that fill behind its transparent pixels when
|
||||
/// the group's shapes take the slide background fill.
|
||||
#[test]
|
||||
fn background_fill_keeps_the_group_fill_a_picture_uses() {
|
||||
let mut s = Session::with_new();
|
||||
s.execute("slide.new", &json!({"layout": "blank"})).unwrap();
|
||||
let shape = s.execute("shape.insert", &json!({"preset": "rect", "rect": [100, 100, 200, 100]})).unwrap()["id"].as_u64().unwrap();
|
||||
let png = deckcraft_render::Image { width: 2, height: 2, pixels: [0, 0, 0, 0].repeat(4) }.to_png();
|
||||
let pic = s
|
||||
.execute("insert.picture", &json!({"data": crate::cmd::base64_encode(&png), "name": "clear.png", "rect": [400, 100, 100, 100]}))
|
||||
.unwrap()["id"]
|
||||
.as_u64()
|
||||
.unwrap();
|
||||
s.execute("edit.select", &json!({"ids": [shape, pic]})).unwrap();
|
||||
let group = s.execute("arrange.group", &json!({})).unwrap()["id"].as_u64().unwrap();
|
||||
let red = Some(Fill::solid(ColorRef::rgb(Rgba::rgb(255, 0, 0))));
|
||||
s.edit(|doc, _| {
|
||||
for slide in &mut doc.slides {
|
||||
let slide = std::sync::Arc::make_mut(slide);
|
||||
if let Some(g) = slide.shape_mut(ShapeId(group as u32)) {
|
||||
g.fill.clone_from(&red);
|
||||
}
|
||||
if let Some(p) = slide.shape_mut(ShapeId(pic as u32)) {
|
||||
p.fill = Some(Fill::Group);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
let behind = |s: &mut Session| s.execute("slide.colorAt", &json!({"x": 450, "y": 150})).unwrap()["color"].clone();
|
||||
assert_eq!(behind(&mut s), "FF0000", "the group's fill shows through the picture");
|
||||
s.execute("edit.select", &json!({"ids": [group]})).unwrap();
|
||||
s.execute("shape.fill", &json!({"background": true})).unwrap();
|
||||
let fill = |s: &Session, id: u64| s.doc().unwrap().shape(ShapeId(id as u32)).unwrap().fill.clone();
|
||||
assert_eq!(fill(&s, shape), Some(Fill::Background));
|
||||
assert_eq!(fill(&s, pic), Some(Fill::Group));
|
||||
assert_eq!(fill(&s, group), red, "the group keeps the fill its picture uses");
|
||||
assert_eq!(behind(&mut s), "FF0000", "the group's fill still shows through the picture");
|
||||
}
|
||||
|
||||
fn rotation(s: &Session) -> f64 {
|
||||
let st = s.active().unwrap();
|
||||
let id = st.selection.shapes[0];
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::{
|
||||
ColorRef, ColorTransform, Dash, Effects, Fill, Geom, Glow, Gradient, GradientShape, GradientStop, Line, LineEnd, PictureFill, PictureMode,
|
||||
Reflection, Result, Rgba, SchemeSlot, Session, Shadow, ShapeStyle, Value, apply_fill, bad, bool_or, color_param, color_value, edit_geometry,
|
||||
edit_shapes, f64_param, inherited_fills, json, requested_fill, str_param, usize_param,
|
||||
edit_shapes, f64_param, inherited_fills, json, requested_fill, str_param, targets, usize_param,
|
||||
};
|
||||
|
||||
pub(super) fn adjust(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
@@ -134,11 +134,47 @@ pub(super) fn fill(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let transparency = f64_param(p, "transparency");
|
||||
let pic_props = p.get("pictureProps").filter(|v| v.is_object());
|
||||
if new_fill.is_none() && transparency.is_none() && pic_props.is_none() {
|
||||
return Err(bad("shape.fill", "give `color`, `none`, `gradient`, `pattern`, `picture`, `pictureProps`, `transparency` or `reset`"));
|
||||
return Err(bad(
|
||||
"shape.fill",
|
||||
"give `color`, `none`, `background`, `gradient`, `pattern`, `picture`, `pictureProps`, `transparency` or `reset`",
|
||||
));
|
||||
}
|
||||
// Transparency on an inherited (style) fill needs the effective colour.
|
||||
let inherited = inherited_fills(s, p)?;
|
||||
edit_group_appearance(s, p, "shape.fill", |sh| apply_fill(sh, new_fill.as_ref(), pic_props, transparency, &inherited))
|
||||
let background = matches!(new_fill, Some(Some(Fill::Background)));
|
||||
if background {
|
||||
let st = s.doc()?;
|
||||
let mut any = false;
|
||||
for id in targets(s, p)? {
|
||||
if let Some(sh) = st.shape(id) {
|
||||
deckcraft_model::walk(std::slice::from_ref(sh), &mut |sh, _| any |= matches!(sh.kind, deckcraft_model::ShapeKind::Shape));
|
||||
}
|
||||
}
|
||||
if !any {
|
||||
return Err(bad("shape.fill", "slide background fill applies to shapes and text boxes"));
|
||||
}
|
||||
}
|
||||
edit_group_appearance(s, p, "shape.fill", |sh| {
|
||||
if background && !matches!(sh.kind, deckcraft_model::ShapeKind::Shape) {
|
||||
if matches!(sh.kind, deckcraft_model::ShapeKind::Group { .. }) && !group_fill_used(sh, 0) {
|
||||
sh.fill = None;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
apply_fill(sh, new_fill.as_ref(), pic_props, transparency, &inherited)
|
||||
})
|
||||
}
|
||||
|
||||
/// Only a shape or text box (`p:sp`) saves a slide background fill (`useBgFill`). Pictures,
|
||||
/// connectors, tables and charts keep their fill, and a group's own fill is cleared unless one
|
||||
/// of them is filled with it (`grpFill`).
|
||||
fn group_fill_used(sh: &deckcraft_model::Shape, depth: usize) -> bool {
|
||||
depth <= 64
|
||||
&& sh.children().iter().any(|c| match c.kind {
|
||||
deckcraft_model::ShapeKind::Shape => false,
|
||||
deckcraft_model::ShapeKind::Group { .. } => matches!(c.fill, None | Some(Fill::Group)) && group_fill_used(c, depth + 1),
|
||||
_ => c.fill == Some(Fill::Group),
|
||||
})
|
||||
}
|
||||
|
||||
/// Applies Format Shape ▸ Picture or texture fill options to a picture fill.
|
||||
|
||||
@@ -300,6 +300,119 @@ mod tests {
|
||||
h.run_steps(2);
|
||||
assert!(alpha(&h).is_some_and(|a| (a - 0.6).abs() < 1e-9), "{:?}", alpha(&h));
|
||||
}
|
||||
|
||||
/// Slide background fill is one of the Fill options. It has no Color button or Transparency
|
||||
/// slider of its own.
|
||||
#[test]
|
||||
fn slide_background_fill_is_a_fill_option() {
|
||||
let mut h = fill_pane(None);
|
||||
let fill = |h: &Harness<'_, SlideApp>| h.state().session.active().unwrap().selected_shapes()[0].fill.clone();
|
||||
let undo = h.state().session.active().unwrap().history.undo.len();
|
||||
h.get_by_label("Slide background fill").click();
|
||||
h.run_steps(3);
|
||||
assert_eq!(fill(&h), Some(Fill::Background));
|
||||
assert_eq!(h.state().session.active().unwrap().history.undo.len(), undo + 1, "one undo step");
|
||||
assert_eq!(h.get_by_label("Slide background fill").accesskit_node().toggled(), Some(egui::accesskit::Toggled::True));
|
||||
assert_eq!(color_buttons(&h), 1, "only the line's Color button");
|
||||
assert_eq!(h.query_all_by_role_and_label(egui::accesskit::Role::Slider, "Transparency").count(), 0);
|
||||
// A shape that already has the background fill shows this option chosen, not Automatic.
|
||||
let h = fill_pane(Some(json!({"background": true})));
|
||||
assert_eq!(h.get_by_label("Slide background fill").accesskit_node().toggled(), Some(egui::accesskit::Toggled::True));
|
||||
let automatic = h.query_all_by_label("Automatic").filter(|n| n.accesskit_node().toggled() == Some(egui::accesskit::Toggled::True)).count();
|
||||
assert_eq!(automatic, 1, "only the line's Automatic is chosen");
|
||||
}
|
||||
|
||||
/// A picture cannot save a slide background fill, so its Fill options leave it out. A group
|
||||
/// that holds a shape keeps the option.
|
||||
#[test]
|
||||
fn a_picture_has_no_slide_background_fill_option() {
|
||||
let pane = |group: bool| {
|
||||
let mut s = Session::with_new();
|
||||
s.execute("slide.new", &json!({"layout": "blank"})).unwrap();
|
||||
let png = deckcraft_render::Image { width: 2, height: 2, pixels: [255, 0, 0, 255].repeat(4) }.to_png();
|
||||
let pic =
|
||||
s.execute("insert.picture", &json!({"data": deckcraft_engine::cmd::base64_encode(&png), "name": "red.png"})).unwrap()["id"].clone();
|
||||
if group {
|
||||
let shape = s.execute("shape.insert", &json!({"preset": "rect", "rect": [500.0, 40.0, 120.0, 80.0]})).unwrap()["id"].clone();
|
||||
s.execute("edit.select", &json!({"ids": [pic, shape]})).unwrap();
|
||||
s.execute("arrange.group", &json!({})).unwrap();
|
||||
s.execute("shape.fill", &json!({"color": "#ff0000"})).unwrap();
|
||||
}
|
||||
let mut app = SlideApp::new(s, Services::default());
|
||||
app.ui.pane = Some("format".into());
|
||||
app.ui.format_tab = "fill".into();
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_ui_state(
|
||||
|ui, app: &mut SlideApp| {
|
||||
app.logic(ui.ctx());
|
||||
app.ui(ui);
|
||||
},
|
||||
app,
|
||||
);
|
||||
h.run_steps(3);
|
||||
h
|
||||
};
|
||||
let h = pane(false);
|
||||
assert!(h.query_by_label("Pattern fill").is_some(), "the Fill options are shown");
|
||||
assert!(h.query_by_label("Slide background fill").is_none());
|
||||
let mut h = pane(true);
|
||||
h.get_by_label("Slide background fill").click();
|
||||
h.run_steps(3);
|
||||
let st = h.state().session.active().unwrap();
|
||||
let group = st.selected_shapes()[0];
|
||||
let fills: Vec<_> = group.children().iter().map(|m| m.fill.clone()).collect();
|
||||
assert_eq!(fills[1], Some(Fill::Background), "the group's shape takes the fill");
|
||||
assert!(matches!(fills[0], Some(Fill::Solid { .. })), "the picture keeps its fill: {:?}", fills[0]);
|
||||
assert_eq!(group.fill, None, "the group's own fill is cleared");
|
||||
assert_eq!(h.get_by_label("Slide background fill").accesskit_node().toggled(), Some(egui::accesskit::Toggled::True));
|
||||
assert_eq!(h.query_all_by_role_and_label(egui::accesskit::Role::Slider, "Transparency").count(), 0);
|
||||
assert_eq!(color_buttons(&h), 1, "only the line's Color button");
|
||||
}
|
||||
|
||||
/// A group whose picture uses the group's fill keeps that fill, and still shows Slide
|
||||
/// background fill chosen once its shapes have it.
|
||||
#[test]
|
||||
fn a_group_that_keeps_its_fill_shows_slide_background_fill() {
|
||||
let mut s = Session::with_new();
|
||||
s.execute("slide.new", &json!({"layout": "blank"})).unwrap();
|
||||
let png = deckcraft_render::Image { width: 2, height: 2, pixels: [0, 0, 0, 0].repeat(4) }.to_png();
|
||||
let pic = s.execute("insert.picture", &json!({"data": deckcraft_engine::cmd::base64_encode(&png), "name": "clear.png"})).unwrap()["id"]
|
||||
.as_u64()
|
||||
.unwrap();
|
||||
let shape = s.execute("shape.insert", &json!({"preset": "rect", "rect": [500.0, 40.0, 120.0, 80.0]})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("edit.select", &json!({"ids": [pic, shape]})).unwrap();
|
||||
s.execute("arrange.group", &json!({})).unwrap();
|
||||
s.execute("shape.fill", &json!({"gradient": {"stops": [[0, "#ff0000"], [1, "#0000ff"]]}})).unwrap();
|
||||
s.edit(|doc, _| {
|
||||
for slide in &mut doc.slides {
|
||||
let slide = std::sync::Arc::make_mut(slide);
|
||||
if let Some(p) = slide.shape_mut(ShapeId(pic as u32)) {
|
||||
p.fill = Some(Fill::Group);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
let mut app = SlideApp::new(s, Services::default());
|
||||
app.ui.pane = Some("format".into());
|
||||
app.ui.format_tab = "fill".into();
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_ui_state(
|
||||
|ui, app: &mut SlideApp| {
|
||||
app.logic(ui.ctx());
|
||||
app.ui(ui);
|
||||
},
|
||||
app,
|
||||
);
|
||||
h.run_steps(3);
|
||||
h.get_by_label("Slide background fill").click();
|
||||
h.run_steps(3);
|
||||
let st = h.state().session.active().unwrap();
|
||||
assert!(matches!(st.selected_shapes()[0].fill, Some(Fill::Gradient(_))), "the group keeps the fill its picture uses");
|
||||
assert_eq!(st.shape(ShapeId(shape as u32)).unwrap().fill, Some(Fill::Background));
|
||||
assert_eq!(h.get_by_label("Slide background fill").accesskit_node().toggled(), Some(egui::accesskit::Toggled::True));
|
||||
assert!(h.query_by_label("Rotate with shape").is_none(), "no editor for the group's kept gradient");
|
||||
assert_eq!(h.query_all_by_role_and_label(egui::accesskit::Role::Slider, "Transparency").count(), 0);
|
||||
assert_eq!(color_buttons(&h), 1, "only the line's Color button");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -133,6 +133,33 @@ fn effects_tab(app: &mut SlideApp, ui: &mut Ui, sh: &deckcraft_model::Shape) {
|
||||
}
|
||||
}
|
||||
|
||||
/// A group keeps no slide background fill of its own. It shows Slide background fill when every
|
||||
/// shape or text box in it has one, including when the group keeps a fill that a picture in it uses.
|
||||
fn members_have_background_fill(sh: &deckcraft_model::Shape) -> bool {
|
||||
if !matches!(sh.kind, ShapeKind::Group { .. }) {
|
||||
return false;
|
||||
}
|
||||
let (mut any, mut all) = (false, true);
|
||||
deckcraft_model::walk(sh.children(), &mut |m, _| {
|
||||
if matches!(m.kind, ShapeKind::Shape) {
|
||||
any = true;
|
||||
all &= m.fill == Some(Fill::Background);
|
||||
}
|
||||
});
|
||||
any && all
|
||||
}
|
||||
|
||||
/// Only a shape or text box saves a slide background fill. A group offers it when it holds one.
|
||||
fn selection_takes_background_fill(app: &SlideApp) -> bool {
|
||||
let mut ok = false;
|
||||
if let Some(st) = app.session.active() {
|
||||
for s in st.selected_shapes() {
|
||||
deckcraft_model::walk(std::slice::from_ref(s), &mut |m, _| ok |= matches!(m.kind, ShapeKind::Shape));
|
||||
}
|
||||
}
|
||||
ok
|
||||
}
|
||||
|
||||
/// Render the shape-format pane with fill/line, effects, size/properties, text, and picture tabs.
|
||||
pub(super) fn format_shape(app: &mut SlideApp, ui: &mut Ui) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
@@ -269,23 +296,31 @@ pub(super) fn format_shape(app: &mut SlideApp, ui: &mut Ui) {
|
||||
_ => {
|
||||
// Fill & Line.
|
||||
ui.label(egui::RichText::new("Fill").font(theme::bold(13.0)));
|
||||
let members_background = members_have_background_fill(&sh);
|
||||
let kind = match &sh.fill {
|
||||
_ if members_background => "background",
|
||||
None => "auto",
|
||||
Some(Fill::None) => "none",
|
||||
Some(Fill::Solid { .. }) => "solid",
|
||||
Some(Fill::Gradient(_)) => "gradient",
|
||||
Some(Fill::Picture(_)) => "picture",
|
||||
Some(Fill::Pattern(_)) => "pattern",
|
||||
Some(Fill::Background) => "background",
|
||||
_ => "auto",
|
||||
};
|
||||
let background_ok = selection_takes_background_fill(app);
|
||||
for (l, k) in [
|
||||
("No fill", "none"),
|
||||
("Solid fill", "solid"),
|
||||
("Gradient fill", "gradient"),
|
||||
("Picture or texture fill", "picture"),
|
||||
("Pattern fill", "pattern"),
|
||||
("Slide background fill", "background"),
|
||||
("Automatic", "auto"),
|
||||
] {
|
||||
if k == "background" && !background_ok {
|
||||
continue;
|
||||
}
|
||||
if ui.radio(kind == k, l).clicked() {
|
||||
match k {
|
||||
"none" => run(app, "shape.fill", json!({"none": true})),
|
||||
@@ -297,6 +332,7 @@ pub(super) fn format_shape(app: &mut SlideApp, ui: &mut Ui) {
|
||||
),
|
||||
"pattern" => run(app, "shape.fill", json!({"pattern": {"preset": "pct25", "fg": "accent1", "bg": "bg1"}})),
|
||||
"picture" => pick_fill_picture(app),
|
||||
"background" => run(app, "shape.fill", json!({"background": true})),
|
||||
_ => run(app, "shape.fill", json!({"reset": true})),
|
||||
}
|
||||
}
|
||||
@@ -312,7 +348,7 @@ pub(super) fn format_shape(app: &mut SlideApp, ui: &mut Ui) {
|
||||
}
|
||||
// `shape.fill {transparency}` changes a solid fill (an automatic one through its
|
||||
// effective color) and a picture fill; a gradient has a transparency per stop.
|
||||
let transparency = matches!(sh.fill, None | Some(Fill::Solid { .. } | Fill::Picture(_)));
|
||||
let transparency = !members_background && matches!(sh.fill, None | Some(Fill::Solid { .. } | Fill::Picture(_)));
|
||||
ui.horizontal(|ui| {
|
||||
if !transparency {
|
||||
return;
|
||||
@@ -336,10 +372,13 @@ pub(super) fn format_shape(app: &mut SlideApp, ui: &mut Ui) {
|
||||
ui.data_mut(|d| d.insert_temp(id, v));
|
||||
}
|
||||
});
|
||||
if let Some(Fill::Picture(pf)) = &sh.fill {
|
||||
// A group that keeps a fill for its pictures shows no editor for that fill while its
|
||||
// shapes have the slide background fill.
|
||||
let shown_fill = sh.fill.as_ref().filter(|_| !members_background);
|
||||
if let Some(Fill::Picture(pf)) = shown_fill {
|
||||
picture_fill_options(app, ui, sh.id.0, pf);
|
||||
}
|
||||
if let Some(Fill::Gradient(g)) = &sh.fill {
|
||||
if let Some(Fill::Gradient(g)) = shown_fill {
|
||||
let sc = scheme(app);
|
||||
if let Some(ng) = crate::fillui::gradient_editor(ui, "shape-grad", g, &sc) {
|
||||
run(app, "shape.fill", json!({"gradient": crate::fillui::gradient_json(&ng)}));
|
||||
@@ -350,7 +389,7 @@ pub(super) fn format_shape(app: &mut SlideApp, ui: &mut Ui) {
|
||||
run(app, "shape.fill", json!({"gradient": crate::fillui::gradient_json(&ng)}));
|
||||
}
|
||||
}
|
||||
if let Some(Fill::Pattern(pt)) = &sh.fill
|
||||
if let Some(Fill::Pattern(pt)) = shown_fill
|
||||
&& let Some(np) = crate::fillui::pattern_editor(ui, pt, &scheme(app))
|
||||
{
|
||||
run(app, "shape.fill", json!({"pattern": crate::fillui::pattern_json(&np)}));
|
||||
|
||||
+1
-1
@@ -65,7 +65,7 @@ menu colours taken from the theme tokens (menu `chrome`, page `card`).
|
||||
|
||||
| Pane / dialog | PowerPoint | DeckCraft |
|
||||
|---|---|---|
|
||||
| Format Shape / Picture / Text pane (Fill & Line, Effects, Size & Properties, Picture, Text Box) | full | partial: fill (solid transparency follows the selection), gradient editor, picture/texture fill (source, tile, offsets, scale, alignment, mirror, transparency, rotate with shape), pattern fill (gallery of the 48 presets PowerPoint offers, foreground and background colors), line (dash, compound, cap, join, begin/end arrows and sizes), shadow/glow/reflection details (colour, transparency, blur, angle, distance, size), soft edges, 3-D format and rotation (bevels, depth, contour, material, lighting, camera presets, X/Y/Z rotation), size (lock aspect ratio) |
|
||||
| Format Shape / Picture / Text pane (Fill & Line, Effects, Size & Properties, Picture, Text Box) | full | partial: fill (solid transparency follows the selection), gradient editor, picture/texture fill (source, tile, offsets, scale, alignment, mirror, transparency, rotate with shape), pattern fill (gallery of the 48 presets PowerPoint offers, foreground and background colors), slide background fill, line (dash, compound, cap, join, begin/end arrows and sizes), shadow/glow/reflection details (colour, transparency, blur, angle, distance, size), soft edges, 3-D format and rotation (bevels, depth, contour, material, lighting, camera presets, X/Y/Z rotation), size (lock aspect ratio) |
|
||||
| Slide Master | Insert Layout, Rename, Delete, Preserve, Insert Placeholder, Title and Footers | Insert Layout, Rename, Delete, Insert Placeholder, and Preserve, Title and Footers checkboxes (`master.preserve`, `master.placeholders`). A master thumbnail selects the master |
|
||||
| Format Background pane | yes | yes: solid transparency; picture transparency, tile, offset, scale, alignment and mirror; Apply to All copies a picture. Stretch insets and rotate-with-shape are command parameters |
|
||||
| Selection Pane | front-most first, groups fold, drag to restack, Bring Forward / Send Backward arrows, Show All / Hide All, eye, padlock (Lock), rename in place | same: drag restacks among siblings (a child stays in its group), Ctrl+click toggles, Shift+click selects a run, arrows / Home / End / F2 / Esc in the list, right-click menu |
|
||||
|
||||
Reference in New Issue
Block a user