mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 18:46:57 -06:00
Shape Fill and Shape Outline: the button applies the last colour picked from its menu (part of #1080)
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
//! Shape Fill and Shape Outline remember the last colour picked from their menu, and a click on
|
||||
//! the button itself applies it again to the next selection, as in PowerPoint (#1080).
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used)]
|
||||
|
||||
use deckcraft_engine::Session;
|
||||
use deckcraft_model::{ColorBase, ColorRef, Fill, ShapeId};
|
||||
use egui::{Event, Key, Modifiers, PointerButton};
|
||||
use egui_kittest::Harness;
|
||||
use egui_kittest::kittest::Queryable;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{Services, SlideApp};
|
||||
|
||||
/// Two rectangles, the first selected; the second starts red with a blue outline.
|
||||
fn harness(tab: &str) -> (Harness<'static, SlideApp>, [ShapeId; 2]) {
|
||||
let mut s = Session::with_new();
|
||||
s.execute("slide.new", &json!({"layout": "blank"})).unwrap();
|
||||
let mut ids = [ShapeId(0); 2];
|
||||
for (i, x) in [100.0, 300.0].iter().enumerate() {
|
||||
let r = s.execute("shape.insert", &json!({"preset": "rect", "rect": [x, 100.0, 80.0, 60.0]})).unwrap();
|
||||
ids[i] = ShapeId(r["id"].as_u64().unwrap() as u32);
|
||||
}
|
||||
s.execute("selection.select", &json!({"ids": [ids[1].0]})).unwrap();
|
||||
s.execute("shape.fill", &json!({"color": "#FF0000"})).unwrap();
|
||||
s.execute("shape.line", &json!({"color": "#0000FF", "width": 3})).unwrap();
|
||||
s.execute("selection.select", &json!({"ids": [ids[0].0]})).unwrap();
|
||||
let mut app = SlideApp::new(s, Services::default());
|
||||
app.ui.tab = tab.into();
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1800.0, 900.0)).build_ui_state(
|
||||
|ui, app: &mut SlideApp| {
|
||||
app.logic(ui.ctx());
|
||||
app.ui(ui);
|
||||
},
|
||||
app,
|
||||
);
|
||||
h.run_steps(3);
|
||||
(h, ids)
|
||||
}
|
||||
|
||||
fn select(h: &mut Harness<'static, SlideApp>, id: ShapeId) {
|
||||
h.state_mut().session.execute("selection.select", &json!({"ids": [id.0]})).unwrap();
|
||||
h.run_steps(3);
|
||||
}
|
||||
|
||||
/// Open the drop-down arrow of a split button, type a hex colour into "More Colors…" and press Enter.
|
||||
fn pick_hex(h: &mut Harness<'static, SlideApp>, button: &str, hex: &str) {
|
||||
let r = h.get_by_label(button).rect();
|
||||
let arrow = egui::pos2(r.max.x - 5.0, r.center().y);
|
||||
h.input_mut().events.push(Event::PointerMoved(arrow));
|
||||
h.run_steps(2);
|
||||
for pressed in [true, false] {
|
||||
h.input_mut().events.push(Event::PointerButton { pos: arrow, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE });
|
||||
h.run_steps(2);
|
||||
}
|
||||
h.run_steps(2);
|
||||
// The ribbon has text fields of its own; the open menu hangs below them.
|
||||
let lowest = |h: &Harness<'static, SlideApp>| {
|
||||
h.get_all_by_role(egui::accesskit::Role::TextInput).max_by(|a, b| a.rect().min.y.total_cmp(&b.rect().min.y)).unwrap().rect().center()
|
||||
};
|
||||
let field = lowest(h);
|
||||
for pressed in [true, false] {
|
||||
h.input_mut().events.push(Event::PointerButton { pos: field, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE });
|
||||
h.run_steps(2);
|
||||
}
|
||||
h.input_mut().events.push(Event::Text(hex.into()));
|
||||
h.run_steps(2);
|
||||
for pressed in [true, false] {
|
||||
h.input_mut().events.push(Event::Key { key: Key::Enter, physical_key: None, pressed, repeat: false, modifiers: Modifiers::NONE });
|
||||
h.run_steps(2);
|
||||
}
|
||||
h.run_steps(2);
|
||||
}
|
||||
|
||||
fn fill_rgb(h: &Harness<'static, SlideApp>, id: ShapeId) -> Option<String> {
|
||||
let sh = h.state().session.active().unwrap().shape(id).unwrap();
|
||||
match sh.fill.as_ref()? {
|
||||
Fill::Solid { color: ColorRef { base: ColorBase::Rgb { rgb }, .. } } => Some(rgb.hex()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn line_rgb(h: &Harness<'static, SlideApp>, id: ShapeId) -> Option<String> {
|
||||
let sh = h.state().session.active().unwrap().shape(id).unwrap();
|
||||
match sh.line.as_ref()?.fill.as_ref()? {
|
||||
Fill::Solid { color: ColorRef { base: ColorBase::Rgb { rgb }, .. } } => Some(rgb.hex()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn fill_reapplies(tab: &str) {
|
||||
let (mut h, [a, b]) = harness(tab);
|
||||
pick_hex(&mut h, "Shape Fill", "00AA55");
|
||||
assert_eq!(fill_rgb(&h, a).as_deref(), Some("00AA55"), "the menu pick applies to the selection");
|
||||
select(&mut h, b);
|
||||
assert_eq!(fill_rgb(&h, b).as_deref(), Some("FF0000"), "selecting alone changes nothing");
|
||||
h.get_by_label("Shape Fill").click();
|
||||
h.run_steps(3);
|
||||
assert_eq!(fill_rgb(&h, b).as_deref(), Some("00AA55"), "the button applies the last colour, not Accent 1");
|
||||
}
|
||||
|
||||
fn outline_reapplies(tab: &str) {
|
||||
let (mut h, [a, b]) = harness(tab);
|
||||
pick_hex(&mut h, "Shape Outline", "AA5500");
|
||||
assert_eq!(line_rgb(&h, a).as_deref(), Some("AA5500"), "the menu pick applies to the selection");
|
||||
select(&mut h, b);
|
||||
assert_eq!(line_rgb(&h, b).as_deref(), Some("0000FF"), "selecting alone changes nothing");
|
||||
h.get_by_label("Shape Outline").click();
|
||||
h.run_steps(3);
|
||||
assert_eq!(line_rgb(&h, b).as_deref(), Some("AA5500"), "the button applies the last colour, not Text 1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn home_shape_fill_button_reapplies_the_last_colour() {
|
||||
fill_reapplies("Home");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_format_shape_fill_button_reapplies_the_last_colour() {
|
||||
fill_reapplies("Shape Format");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn home_shape_outline_button_reapplies_the_last_colour() {
|
||||
outline_reapplies("Home");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_format_shape_outline_button_reapplies_the_last_colour() {
|
||||
outline_reapplies("Shape Format");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn before_any_pick_the_buttons_still_apply_accent_1_and_text_1() {
|
||||
let (mut h, [a, _]) = harness("Home");
|
||||
h.get_by_label("Shape Fill").click();
|
||||
h.run_steps(3);
|
||||
h.get_by_label("Shape Outline").click();
|
||||
h.run_steps(3);
|
||||
let sh = h.state().session.active().unwrap().shape(a).unwrap();
|
||||
assert!(matches!(&sh.fill, Some(Fill::Solid { color: ColorRef { base: ColorBase::Scheme { .. }, .. } })));
|
||||
assert!(sh.line.is_some());
|
||||
}
|
||||
@@ -433,6 +433,10 @@ struct AnimPreview {
|
||||
}
|
||||
|
||||
pub struct SlideApp {
|
||||
/// Shape Fill's last colour (a command colour parameter): the button applies it again. Accent 1 until one is picked.
|
||||
pub last_fill: serde_json::Value,
|
||||
/// Shape Outline's last colour, likewise. Text 1 until one is picked.
|
||||
pub last_outline: serde_json::Value,
|
||||
/// Table Design's border pen (`color`, `width`, `dash` for `table.borders`).
|
||||
pub table_pen: serde_json::Value,
|
||||
pub session: Session,
|
||||
@@ -533,6 +537,8 @@ impl SlideApp {
|
||||
pub fn new(session: Session, mut services: Services) -> Self {
|
||||
let audio_out = services.audio_out.take();
|
||||
SlideApp {
|
||||
last_fill: serde_json::json!("accent1"),
|
||||
last_outline: serde_json::json!("tx1"),
|
||||
table_pen: serde_json::json!({"width": 1.0}),
|
||||
session,
|
||||
services,
|
||||
@@ -2056,6 +2062,8 @@ mod huge_deck_tests;
|
||||
#[cfg(test)]
|
||||
mod ime_tests;
|
||||
#[cfg(test)]
|
||||
mod last_color_tests;
|
||||
#[cfg(test)]
|
||||
mod replacefonts_tests;
|
||||
#[cfg(test)]
|
||||
mod slide_size_tests;
|
||||
|
||||
@@ -925,13 +925,17 @@ fn drawing_group(app: &mut SlideApp, ui: &mut Ui) {
|
||||
let sc = scheme(app);
|
||||
let (m, a) = small_button_ex(ui, Icon::ShapeFill, "Shape Fill", sel, false, true);
|
||||
if m.clicked() {
|
||||
run(app, "shape.fill", json!({"color": "accent1"}));
|
||||
let c = app.last_fill.clone();
|
||||
run(app, "shape.fill", json!({"color": c}));
|
||||
}
|
||||
if let Some(a) = a {
|
||||
widgets::color_popup(&a).show(|ui| {
|
||||
if let Some(c) = widgets::color_grid(ui, &sc, Some("No Fill")) {
|
||||
match c {
|
||||
Some(c) => run(app, "shape.fill", json!({"color": cref_param(&c)})),
|
||||
Some(c) => {
|
||||
app.last_fill = cref_param(&c);
|
||||
run(app, "shape.fill", json!({"color": cref_param(&c)}));
|
||||
}
|
||||
None => run(app, "shape.fill", json!({"none": true})),
|
||||
}
|
||||
}
|
||||
@@ -939,7 +943,8 @@ fn drawing_group(app: &mut SlideApp, ui: &mut Ui) {
|
||||
}
|
||||
let (m, a) = small_button_ex(ui, Icon::ShapeOutline, "Shape Outline", sel, false, true);
|
||||
if m.clicked() {
|
||||
run(app, "shape.line", json!({"color": "tx1", "width": 1}));
|
||||
let c = app.last_outline.clone();
|
||||
run(app, "shape.line", json!({"color": c, "width": 1}));
|
||||
}
|
||||
if let Some(a) = a {
|
||||
widgets::color_popup(&a).show(|ui| outline_menu(app, ui, &sc));
|
||||
@@ -955,7 +960,10 @@ fn drawing_group(app: &mut SlideApp, ui: &mut Ui) {
|
||||
pub fn outline_menu(app: &mut SlideApp, ui: &mut Ui, sc: &deckcraft_color::ColorScheme) {
|
||||
if let Some(c) = widgets::color_grid(ui, sc, Some("No Outline")) {
|
||||
match c {
|
||||
Some(c) => run(app, "shape.line", json!({"color": cref_param(&c)})),
|
||||
Some(c) => {
|
||||
app.last_outline = cref_param(&c);
|
||||
run(app, "shape.line", json!({"color": cref_param(&c)}));
|
||||
}
|
||||
None => run(app, "shape.line", json!({"none": true})),
|
||||
}
|
||||
}
|
||||
@@ -2436,13 +2444,17 @@ fn shape_format(app: &mut SlideApp, ui: &mut Ui) {
|
||||
rows(ui, |ui| {
|
||||
let (m, a) = small_button_ex(ui, Icon::ShapeFill, "Shape Fill", sel, false, true);
|
||||
if m.clicked() {
|
||||
run(app, "shape.fill", json!({"color": "accent1"}));
|
||||
let c = app.last_fill.clone();
|
||||
run(app, "shape.fill", json!({"color": c}));
|
||||
}
|
||||
if let Some(a) = a {
|
||||
widgets::color_popup(&a).show(|ui| {
|
||||
if let Some(c) = widgets::color_grid(ui, &sc, Some("No Fill")) {
|
||||
match c {
|
||||
Some(c) => run(app, "shape.fill", json!({"color": cref_param(&c)})),
|
||||
Some(c) => {
|
||||
app.last_fill = cref_param(&c);
|
||||
run(app, "shape.fill", json!({"color": cref_param(&c)}));
|
||||
}
|
||||
None => run(app, "shape.fill", json!({"none": true})),
|
||||
}
|
||||
}
|
||||
@@ -2452,7 +2464,11 @@ fn shape_format(app: &mut SlideApp, ui: &mut Ui) {
|
||||
}
|
||||
});
|
||||
}
|
||||
let (_, a) = small_button_ex(ui, Icon::ShapeOutline, "Shape Outline", sel, false, true);
|
||||
let (m, a) = small_button_ex(ui, Icon::ShapeOutline, "Shape Outline", sel, false, true);
|
||||
if m.clicked() {
|
||||
let c = app.last_outline.clone();
|
||||
run(app, "shape.line", json!({"color": c, "width": 1}));
|
||||
}
|
||||
if let Some(a) = a {
|
||||
widgets::color_popup(&a).show(|ui| outline_menu(app, ui, &sc));
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ ends and are omitted from thumbnails, slide shows and exports.
|
||||
|
||||
| Element | PowerPoint for Mac | DeckCraft | Gap |
|
||||
|---|---|---|---|
|
||||
| 2026-10-23 | minor | Shape Fill and Shape Outline (Home and Shape Format): the button applies the last colour picked from its menu, Accent 1 and Text 1 until one is picked. Line weight and dashes, Font Color and Highlight stay as they were (#1080) |
|
||||
| Title bar: AutoSave, Home, Save, Undo/Redo, document title, Share, search | yes | yes, plus Export; no Share (cloud) | #71 macOS window controls misaligned |
|
||||
| Native macOS menu bar (File, Edit, View, Insert, Format, Arrange, Tools, Slide Show, Window, Help) | yes | **no** (`menus::menu_tree()` is defined but not installed) | 3–5 h |
|
||||
| Menu-bar items with a DeckCraft equivalent | 217 app-specific items | 128 matched by name (59%, heuristic lower bound) | See [target-app-parity.md](target-app-parity.md) |
|
||||
|
||||
Reference in New Issue
Block a user