mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 10:37:03 -06:00
Merge PR #697 from NikitkaRa: UI: Format Shape fill transparency follows the selection
# Conflicts: # crates/ui-egui/src/panes.rs
This commit is contained in:
@@ -35,6 +35,7 @@
|
||||
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** trivial (progress log: chart trendlines are drawn; previous: trivial, chart trendlines round-trip; hours to ~95% per audience added; ready ~55%; stage stays alpha; full re-measure against PowerPoint for Mac 16.113.4) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** trivial (progress log: copy and paste rich text and pictures with other apps; previous: hours to ~95% per audience added) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** trivial (animation preview on the canvas; breadth 79% → 80%, Animations 79% → 85%; stage stays alpha) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** trivial (progress log: Format Shape fill transparency follows the selection; breadth stays 79%; previous: minor, hours to ~95%) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
|
||||
DeckCraft aims at full PowerPoint parity, and to be better: faster, open (a documented zip+JSON
|
||||
format plus PPTX), scriptable by agents (CLI, JSON control channel, MCP), and available everywhere
|
||||
@@ -218,6 +219,7 @@ app's JSON control channel (`deckcraft --control PORT`).
|
||||
| 2026-10-13 | Animations ▸ Preview plays the current slide's animations on the editor canvas (Play All does the same); Escape or a click stops it. Breadth 79% → 80%, Animations 79% → 85% |
|
||||
| 2026-10-14 | A two-finger twist on the trackpad rotates the selected shapes, as one undo step. Pinch-zoom is unchanged |
|
||||
| 2026-10-14 | A trackpad swipe turns the page: sideways when the slide fits the window, and any flick during the slide show. One gesture turns one page. Pinch-zoom is unchanged |
|
||||
| 2026-10-14 | Format Shape: the Fill transparency slider shows the selected solid fill, and leaving it no longer clears that transparency |
|
||||
| 2026-10-13 | Slide transitions like PowerPoint: every transition and option measured frame by frame against PowerPoint's video export and retuned (curves, directions, soft edges, 3-D perspective, contents moving over backgrounds); PowerPoint's own XML elements and defaults read and written (diagonal Wipe, Zoom, Shape In/Out, Glitter Hexagons, Shred In/Out, Page Curl Single); `deckcraft-cli render-transition` renders frames headless |
|
||||
| 2026-10-13 | Wild text cases measured against PowerPoint (102 synthetic slides): bidi by run language, East Asian vertical text upright, mongolianVert/wordArtVert/wordArtVertRtl, underline styles and strikethrough at the font's metrics, centre/right/decimal tabs, East Asian, Thai, Hindi, Arabic and Hebrew numbering, small caps, all-caps "ß", transparent text, soft hyphens, kinsoku, rtlCol and just/dist anchoring, emoji presentation, fallback faces that no longer move the baseline |
|
||||
| 2026-10-13 | Render matches PowerPoint more closely on wild shape, picture and table cases: even-odd custom paths, dashes and caps, arrowheads, radial and path gradients, tile rects, rotWithShape, gradient lines, shadows cast by what is drawn (group and perspective too), group fill, picture borders, tiles, reflection, recolour (biLevel, alphaInv, brightness/contrast), table borders and grid default, 20+ preset shapes by their ECMA formulas, default miter joins, and approximate 3-D bevels, cameras and face lighting |
|
||||
@@ -286,6 +288,7 @@ app's JSON control channel (`deckcraft --control PORT`).
|
||||
| 2026-10-14 | trivial | Progress log: Trim Media dialog follows the selection |
|
||||
| 2026-10-14 | trivial | Progress log: copy and paste rich text and pictures with other apps |
|
||||
| 2026-10-13 | trivial | Progress log: animation preview on the editor canvas; breadth 80%, Animations 85% |
|
||||
| 2026-10-14 | trivial | Progress log: Format Shape fill transparency follows the selection |
|
||||
| 2026-10-13 | trivial | Progress log: wild text cases |
|
||||
| 2026-10-13 | trivial | Progress log: slide show for interactive decks |
|
||||
| 2026-10-12 | trivial | Progress log: File tab (Backstage, #76) |
|
||||
|
||||
+123
-4
@@ -151,6 +151,29 @@ fn run(app: &mut SlideApp, id: &str, p: Value) {
|
||||
let _ = app.run(id, p);
|
||||
}
|
||||
|
||||
/// Percent transparency (0 = opaque) of a solid fill, the same number `shape.fill`'s
|
||||
/// `transparency` parameter writes as an alpha transform. Anything else — no fill, a
|
||||
/// gradient, a picture, a pattern — is 0, because that command only changes a solid.
|
||||
fn solid_fill_transparency_percent(fill: Option<&Fill>) -> f32 {
|
||||
let Some(Fill::Solid { color }) = fill else {
|
||||
return 0.0;
|
||||
};
|
||||
((1.0 - color.alpha()) * 100.0) as f32
|
||||
}
|
||||
|
||||
/// Fraction to pass to `shape.fill` when the slider moved. An unchanged value (a focus
|
||||
/// loss, or switching to another shape) is `None`, so the pane does not rewrite the fill.
|
||||
fn fill_transparency_to_apply(shown_percent: f32, seed_percent: f32) -> Option<f64> {
|
||||
if !shown_percent.is_finite() {
|
||||
return None;
|
||||
}
|
||||
let shown = shown_percent.clamp(0.0, 100.0);
|
||||
if (shown - seed_percent).abs() <= 0.01 {
|
||||
return None;
|
||||
}
|
||||
Some(f64::from(shown) / 100.0)
|
||||
}
|
||||
|
||||
fn scheme(app: &SlideApp) -> deckcraft_color::ColorScheme {
|
||||
app.session.active().and_then(|d| d.doc.masters.first().map(|m| m.scheme())).unwrap_or_else(|| deckcraft_model::Theme::default().colors)
|
||||
}
|
||||
@@ -446,12 +469,19 @@ fn format_shape(app: &mut SlideApp, ui: &mut Ui) {
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Transparency");
|
||||
let id = ui.id().with("ftr");
|
||||
let mut v: f32 = ui.data_mut(|d| d.get_temp(id).unwrap_or(0.0));
|
||||
// Seed from this shape. A shared temp defaulted to 0, so a shape that was
|
||||
// already 40% transparent showed as opaque, and leaving the slider wrote 0.
|
||||
let seed = solid_fill_transparency_percent(sh.fill.as_ref());
|
||||
let id = ui.id().with(("ftr", sh.id.0));
|
||||
let mut v: f32 = ui.data_mut(|d| d.get_temp(id).unwrap_or(seed));
|
||||
let r = ui.add(egui::Slider::new(&mut v, 0.0..=100.0).suffix("%"));
|
||||
ui.data_mut(|d| d.insert_temp(id, v));
|
||||
if r.drag_stopped() || r.lost_focus() {
|
||||
let _ = app.run("shape.fill", json!({"transparency": v as f64 / 100.0}));
|
||||
if let Some(t) = fill_transparency_to_apply(v, seed) {
|
||||
let _ = app.run("shape.fill", json!({"transparency": t}));
|
||||
}
|
||||
ui.data_mut(|d| d.remove::<f32>(id));
|
||||
} else if r.dragged() || r.changed() {
|
||||
ui.data_mut(|d| d.insert_temp(id, v));
|
||||
}
|
||||
});
|
||||
if let Some(Fill::Gradient(g)) = &sh.fill {
|
||||
@@ -1087,3 +1117,92 @@ pub(crate) fn text_transparency_slider(app: &mut SlideApp, ui: &mut Ui, sh: &dec
|
||||
#[cfg(test)]
|
||||
#[path = "smartart_tests.rs"]
|
||||
mod smartart_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
|
||||
use deckcraft_color::{ColorTransform, SchemeSlot};
|
||||
use deckcraft_engine::Session;
|
||||
use deckcraft_model::{ColorRef, Fill, ShapeId};
|
||||
use egui_kittest::Harness;
|
||||
use egui_kittest::kittest::{NodeT, Queryable};
|
||||
use serde_json::json;
|
||||
|
||||
use super::{fill_transparency_to_apply, solid_fill_transparency_percent};
|
||||
use crate::{Services, SlideApp};
|
||||
|
||||
#[test]
|
||||
fn solid_fill_transparency_reads_the_alpha_shape_fill_writes() {
|
||||
assert_eq!(solid_fill_transparency_percent(None), 0.0);
|
||||
assert_eq!(solid_fill_transparency_percent(Some(&Fill::None)), 0.0);
|
||||
assert_eq!(solid_fill_transparency_percent(Some(&Fill::Group)), 0.0);
|
||||
let plain = Fill::solid(ColorRef::scheme(SchemeSlot::Accent1));
|
||||
assert_eq!(solid_fill_transparency_percent(Some(&plain)), 0.0);
|
||||
// `shape.fill` transparency 0.4 stores Alpha((1 - 0.4) * 100_000).
|
||||
let forty = Fill::solid(ColorRef::scheme(SchemeSlot::Accent1).with(ColorTransform::Alpha(60_000)));
|
||||
let p = solid_fill_transparency_percent(Some(&forty));
|
||||
assert!((p - 40.0).abs() < 0.01, "{p}");
|
||||
let clear = Fill::solid(ColorRef::scheme(SchemeSlot::Accent1).with(ColorTransform::Alpha(0)));
|
||||
assert!((solid_fill_transparency_percent(Some(&clear)) - 100.0).abs() < 0.01);
|
||||
// A later alpha wins, matching ColorRef::alpha.
|
||||
let layered = Fill::solid(ColorRef::scheme(SchemeSlot::Accent1).with(ColorTransform::Alpha(0)).with(ColorTransform::Alpha(60_000)));
|
||||
assert!((solid_fill_transparency_percent(Some(&layered)) - 40.0).abs() < 0.01);
|
||||
|
||||
assert_eq!(fill_transparency_to_apply(40.0, 40.0), None);
|
||||
assert_eq!(fill_transparency_to_apply(40.005, 40.0), None);
|
||||
assert_eq!(fill_transparency_to_apply(f32::NAN, 40.0), None);
|
||||
assert_eq!(fill_transparency_to_apply(55.0, 40.0), Some(0.55));
|
||||
assert_eq!(fill_transparency_to_apply(0.0, 40.0), Some(0.0));
|
||||
}
|
||||
|
||||
fn solid_alpha(app: &SlideApp, id: u32) -> Option<i32> {
|
||||
let sh = app.session.active()?.shape(ShapeId(id))?;
|
||||
let Fill::Solid { color } = sh.fill.as_ref()? else { return None };
|
||||
color.mods.iter().find_map(|m| match m {
|
||||
ColorTransform::Alpha(v) => Some(*v),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
fn slider_values(h: &Harness<'_, SlideApp>) -> Vec<f64> {
|
||||
h.query_all_by_role(egui::accesskit::Role::Slider).filter_map(|n| n.accesskit_node().numeric_value()).collect()
|
||||
}
|
||||
|
||||
/// The Fill slider shows the selected solid's transparency, and opening the pane
|
||||
/// (or looking at another shape first) does not write 0% over it.
|
||||
#[test]
|
||||
fn fill_transparency_slider_follows_the_selection() {
|
||||
let mut s = Session::with_new();
|
||||
s.execute("slide.new", &json!({"layout": "blank"})).unwrap();
|
||||
let a = s.execute("shape.insert", &json!({"preset": "rect", "rect": [40.0, 40.0, 120.0, 80.0]})).unwrap()["id"].as_u64().unwrap() as u32;
|
||||
let b = s.execute("shape.insert", &json!({"preset": "rect", "rect": [200.0, 40.0, 120.0, 80.0]})).unwrap()["id"].as_u64().unwrap() as u32;
|
||||
s.execute("shape.fill", &json!({"id": a, "color": "accent1", "transparency": 0.0})).unwrap();
|
||||
s.execute("shape.fill", &json!({"id": b, "color": "accent1", "transparency": 0.4})).unwrap();
|
||||
s.execute("edit.select", &json!({"ids": [a]})).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);
|
||||
let opaque: Vec<f64> = slider_values(&h);
|
||||
assert!(opaque.iter().any(|v| (*v - 0.0).abs() < 0.5), "opaque shape should show 0%, got {opaque:?}");
|
||||
assert!(opaque.iter().all(|v| (*v - 40.0).abs() > 0.5), "opaque shape should not show 40%, got {opaque:?}");
|
||||
|
||||
h.state_mut().session.execute("edit.select", &json!({"ids": [b]})).unwrap();
|
||||
h.run_steps(3);
|
||||
let clear: Vec<f64> = slider_values(&h);
|
||||
assert!(clear.iter().any(|v| (*v - 40.0).abs() < 0.5), "40% fill should show 40%, got {clear:?}");
|
||||
assert_eq!(solid_alpha(h.state(), b), Some(60_000));
|
||||
|
||||
h.get_by_label("Transparency").click();
|
||||
h.run_steps(2);
|
||||
assert_eq!(solid_alpha(h.state(), b), Some(60_000));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** trivial (Trim Media dialog follows the selection; previous: File tab / Backstage, #76) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-14 · **Change:** minor (copy and paste rich text and pictures with other apps; previous: text editing keys, #268, and File tab, #76) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** minor (animation preview on the editor canvas; previous: File tab / Backstage, #76) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** trivial (Format Shape fill transparency follows the selection; previous: minor, File tab / Backstage, #76) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
|
||||
> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-14 · **Change:** minor (File › New starter decks; previous: minor, text editing keys observed in PowerPoint for Windows, #268) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (Slide Sorter sections: add, rename, collapse, drag a whole section; previous: text editing keys, #268) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
@@ -91,6 +92,7 @@ menu colours taken from the theme tokens (menu `chrome`, page `card`).
|
||||
|---|---|---|
|
||||
| Format Shape / Picture / Text pane (Fill & Line, Effects, Size & Properties, Picture, Text Box) | full | partial: fill, gradient editor, line (dash, compound, cap, join, begin/end arrows and sizes), some effects, size (lock aspect ratio); no 3-D, thin picture/texture options |
|
||||
| Format Shape / Picture / Text pane (Fill & Line, Effects, Size & Properties, Picture, Text Box) | full | partial: fill, gradient editor, line, shadow/glow/reflection details (colour, transparency, blur, angle, distance, size), soft edges, size; no 3-D, thin picture/texture options |
|
||||
| Format Shape / Picture / Text pane (Fill & Line, Effects, Size & Properties, Picture, Text Box) | full | partial: fill (solid transparency follows the selection), gradient editor, line, some effects, size; no 3-D, thin picture/texture options |
|
||||
| Format Background pane | yes | yes |
|
||||
| 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 |
|
||||
| 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 |
|
||||
@@ -228,6 +230,7 @@ on the slide as shapes. No pixel comparison against PowerPoint screenshots has b
|
||||
| 2026-10-14 | trivial | Trim Media dialog: start, end and fades follow the selected clip |
|
||||
| 2026-10-14 | minor | Copy and paste rich text (HTML: bold, italic, underline) with other apps |
|
||||
| 2026-10-13 | minor | Animations ▸ Preview and Animation Pane ▸ Play All play the current slide on the editor canvas |
|
||||
| 2026-10-14 | trivial | Format Shape Fill transparency slider follows the selected solid fill and does not clear it on focus loss |
|
||||
| 2026-10-12 | minor | File tab (Backstage) observed in PowerPoint for Windows and matched (#76); the start screen is File › Home |
|
||||
| 2026-10-12 | minor | Off-slide objects drawn and editable around the slide in Normal view (#270) |
|
||||
| 2026-10-12 | minor | Selection Pane matched to PowerPoint (#133): drag and drop, arrows, groups, eye, Lock |
|
||||
|
||||
Reference in New Issue
Block a user