Merge pull request #1233 from marcoarnulfo/fix/set-transparent-color-pick

This commit is contained in:
Brandon Thomas
2026-10-23 17:10:41 -04:00
6 changed files with 341 additions and 38 deletions
+107 -3
View File
@@ -1,8 +1,9 @@
//! Picture Format ▸ Color: Recolor presets and Set Transparent Color.
use deckcraft_color::{Rgba, SchemeSlot};
use deckcraft_model::style::PictureAdjust;
use deckcraft_model::{ColorRef, ShapeKind};
use deckcraft_geom::{Affine, Point, Rect, Xfrm};
use deckcraft_model::style::{PictureAdjust, PictureMode};
use deckcraft_model::{ColorRef, Shape, ShapeId, ShapeKind};
use serde_json::{Value, json};
use super::*;
@@ -27,7 +28,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Set Transparent Color",
["Picture Format", "Adjust", "Color"],
None,
"{color: #RRGGBB | none: true, ids?} (pixels of that colour become transparent)",
"{color: #RRGGBB | at: [x, y] | none: true, ids?} (pixels of that colour become transparent; `at` takes the colour of the picture's own pixel under that slide point, as a click does in PowerPoint)",
has_selection,
transparent_color
),
@@ -121,6 +122,15 @@ fn recolor(s: &mut Session, p: &Value) -> Result<Value> {
}
fn transparent_color(s: &mut Session, p: &Value) -> Result<Value> {
if let Some(at) = p.get("at") {
let (id, c) = picture_pixel_at(s, p, at)?;
return edit_shapes(s, &json!({"ids": [id]}), "picture.transparentColor", |sh| {
if let ShapeKind::Picture { fill } = &mut sh.kind {
fill.adjust.clear_color = Some(c);
}
Ok(())
});
}
let c = if bool_or(p, "none", false) {
None
} else {
@@ -134,3 +144,97 @@ fn transparent_color(s: &mut Session, p: &Value) -> Result<Value> {
Ok(())
})
}
/// The topmost target picture under slide point `at` and the colour of its own pixel there: the
/// picture as stored, before corrections and recolouring, so the colour matches the pixels that
/// `clear_color` is compared with.
fn picture_pixel_at(s: &Session, p: &Value, at: &Value) -> Result<(ShapeId, Rgba)> {
const CMD: &str = "picture.transparentColor";
let xy = |i: usize| at.get(i).and_then(Value::as_f64).filter(|v| v.is_finite());
let (Some(x), Some(y)) = (xy(0), xy(1)) else {
return Err(bad(CMD, "`at` is [x, y] in slide units"));
};
let mut ids = targets(s, p)?;
let st = s.doc()?;
let shapes = current_shapes(&st.doc, &st.selection).ok_or_else(|| bad(CMD, "no slide"))?;
// Same order as the hit-test: later in the shape list is in front, and a group's children sit
// where the group is. The selection order says nothing about what is on top.
ids.sort_by_cached_key(|id| std::cmp::Reverse(stack_path(shapes, *id)));
for id in ids {
let Some((sh, xf, to_slide)) = placed(&st.doc, &st.selection, shapes, id, Affine::IDENTITY, 0) else { continue };
let ShapeKind::Picture { fill } = &sh.kind else { continue };
// The camera turns the picture where it is drawn, so the click is mapped back through it too.
let turn = text::ctx_for(&st.doc, &st.selection).and_then(|c| deckcraft_render::camera_turn(&c, sh, xf.w, xf.h)).unwrap_or(Affine::IDENTITY);
let local = (to_slide * turn).inverse() * Point::new(x, y);
if !(local.is_finite() && (0.0..=xf.w).contains(&local.x) && (0.0..=xf.h).contains(&local.y)) {
continue;
}
let PictureMode::Stretch { fill_rect: [l, t, r, b] } = fill.mode else {
return Err(bad(CMD, "a tiled picture has no single pixel under a point; give `color`"));
};
let inner = Rect::new(l * xf.w, t * xf.h, xf.w - r * xf.w, xf.h - b * xf.h);
let dest = deckcraft_render::crop_dest(inner, fill.crop);
let img =
st.doc.media(fill.media).and_then(|m| deckcraft_render::decode_image(&m.data)).ok_or_else(|| bad(CMD, "the picture can't be read"))?;
let (u, v) = ((local.x - dest.x0) / dest.width(), (local.y - dest.y0) / dest.height());
if !((0.0..=1.0).contains(&u) && (0.0..=1.0).contains(&v)) {
continue;
}
let (w, h) = (img.width(), img.height());
let px = ((u * f64::from(w)) as u32).min(w.saturating_sub(1));
let py = ((v * f64::from(h)) as u32).min(h.saturating_sub(1));
let Some(c) = img.get_pixel_checked(px, py) else { continue };
// A transparent pixel keeps a colour nobody sees (often black): clearing it would clear
// that colour where it is drawn.
if c[3] == 0 {
return Err(bad(CMD, "that spot of the picture is already transparent"));
}
return Ok((id, Rgba::rgb(c[0], c[1], c[2])));
}
Err(bad(CMD, "the point is not on a selected picture"))
}
/// Where `id` sits in the paint order: its index in each list from the slide down to it. A larger
/// path is drawn later, so in front.
fn stack_path(shapes: &[Shape], id: ShapeId) -> Option<Vec<usize>> {
for (i, sh) in shapes.iter().enumerate() {
if sh.id == id {
return Some(vec![i]);
}
if let ShapeKind::Group { children, .. } = &sh.kind
&& let Some(mut rest) = stack_path(children, id)
{
rest.insert(0, i);
return Some(rest);
}
}
None
}
/// Shape `id` among `shapes` (searched into groups), its box, and the map from that box to slide
/// space.
fn placed<'a>(
doc: &deckcraft_model::Presentation,
sel: &crate::Selection,
shapes: &'a [Shape],
id: ShapeId,
to_slide: Affine,
depth: usize,
) -> Option<(&'a Shape, Xfrm, Affine)> {
if depth > 64 {
return None;
}
for sh in shapes {
let x = xfrm_of(doc, sel, sh);
if sh.id == id {
return Some((sh, x, to_slide * x.affine()));
}
if let ShapeKind::Group { children, child } = &sh.kind {
let ch = Rect::new(child.x, child.y, child.x + child.w, child.y + child.h);
if let Some(found) = placed(doc, sel, children, id, to_slide * deckcraft_geom::group_child_affine(&x, ch), depth + 1) {
return Some(found);
}
}
}
None
}
+130
View File
@@ -2395,6 +2395,136 @@ fn picture_recolor_and_transparent_color() {
assert_eq!(adj(&s), deckcraft_model::style::PictureAdjust::default());
}
#[test]
fn transparent_color_at_a_point_takes_the_pictures_own_pixel_there() {
// Red on the left half, blue on the right.
let img = image::RgbaImage::from_fn(4, 2, |x, _| if x < 2 { image::Rgba([255, 0, 0, 255]) } else { image::Rgba([0, 0, 255, 255]) });
let mut png = vec![];
image::DynamicImage::ImageRgba8(img).write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap();
let mut s = session();
let id =
s.execute("insert.picture", &json!({"data": cmd::base64_encode(&png), "name": "a.png", "rect": [100, 100, 400, 200]})).unwrap()["id"].clone();
let clear = |s: &Session| match &s.doc().unwrap().shapes().iter().find(|sh| json!(sh.id) == id).unwrap().kind {
deckcraft_model::ShapeKind::Picture { fill } => fill.adjust.clear_color,
_ => panic!("not a picture"),
};
let red = deckcraft_color::Rgba::rgb(255, 0, 0);
let blue = deckcraft_color::Rgba::rgb(0, 0, 255);
s.execute("picture.transparentColor", &json!({"ids": [id], "at": [450, 200]})).unwrap();
assert_eq!(clear(&s), Some(blue));
// The picture's own colour, not the one drawn after a correction brightened it.
s.execute("picture.adjust", &json!({"ids": [id], "brightness": 0.4})).unwrap();
s.execute("picture.transparentColor", &json!({"ids": [id], "at": [150, 200]})).unwrap();
assert_eq!(clear(&s), Some(red));
// Flipped, the blue half is drawn on the left.
s.execute("shape.flip", &json!({"ids": [id], "axis": "horizontal"})).unwrap();
s.execute("picture.transparentColor", &json!({"ids": [id], "at": [150, 200]})).unwrap();
assert_eq!(clear(&s), Some(blue));
// Beside the picture there is no colour to take, and nothing changes.
assert!(s.execute("picture.transparentColor", &json!({"ids": [id], "at": [50, 50]})).is_err());
assert_eq!(clear(&s), Some(blue));
// A spot that is already transparent hides a colour (black here, as in most PNGs): taking it
// would clear the black drawn elsewhere in the picture.
let img = image::RgbaImage::from_fn(4, 2, |x, _| if x < 2 { image::Rgba([0, 0, 0, 0]) } else { image::Rgba([0, 0, 0, 255]) });
let mut png = vec![];
image::DynamicImage::ImageRgba8(img).write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap();
let logo =
s.execute("insert.picture", &json!({"data": cmd::base64_encode(&png), "name": "b.png", "rect": [100, 400, 400, 200]})).unwrap()["id"].clone();
assert!(s.execute("picture.transparentColor", &json!({"ids": [logo], "at": [150, 500]})).is_err());
s.execute("picture.transparentColor", &json!({"ids": [logo], "at": [450, 500]})).unwrap();
}
/// A picture of `n` one-pixel columns whose red channel is 20 * column, inserted at `rect`.
fn ramp_picture(s: &mut Session, n: u32, rect: [u32; 4]) -> serde_json::Value {
let img = image::RgbaImage::from_fn(n, 1, |x, _| image::Rgba([(20 * x) as u8, 0, 0, 255]));
let mut png = vec![];
image::DynamicImage::ImageRgba8(img).write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap();
s.execute("insert.picture", &json!({"data": cmd::base64_encode(&png), "name": "r.png", "rect": rect})).unwrap()["id"].clone()
}
fn clear_color_of(s: &Session, id: &serde_json::Value) -> Option<deckcraft_color::Rgba> {
fn find(shapes: &[deckcraft_model::Shape], id: &serde_json::Value) -> Option<deckcraft_model::Shape> {
shapes.iter().find_map(|sh| {
if json!(sh.id) == *id {
return Some(sh.clone());
}
match &sh.kind {
deckcraft_model::ShapeKind::Group { children, .. } => find(children, id),
_ => None,
}
})
}
match find(s.doc().unwrap().shapes(), id).unwrap().kind {
deckcraft_model::ShapeKind::Picture { fill } => fill.adjust.clear_color,
_ => panic!("not a picture"),
}
}
#[test]
fn transparent_color_at_a_point_follows_the_pictures_3d_camera() {
let mut s = session();
let id = ramp_picture(&mut s, 10, [100, 100, 200, 100]);
// Turned 60 degrees about the vertical axis the picture is drawn about half as wide, centred
// where it was: 149..251 instead of 100..300.
s.execute("shape.effects", &json!({"ids": [id], "threeD": {"camera": "orthographicFront", "rotX": 60}})).unwrap();
// 175 is a quarter of the way across the drawn picture: column 2, not column 3 (the 75/200
// of the unturned box).
s.execute("picture.transparentColor", &json!({"ids": [id], "at": [175, 150]})).unwrap();
assert_eq!(clear_color_of(&s, &id), Some(deckcraft_color::Rgba::rgb(40, 0, 0)));
// Inside the unturned box but outside the drawn picture: nothing is under the pointer.
assert!(s.execute("picture.transparentColor", &json!({"ids": [id], "at": [120, 150]})).is_err());
}
#[test]
fn transparent_color_at_a_point_finds_a_grouped_picture_without_its_own_box() {
let mut s = session();
let pic = ramp_picture(&mut s, 10, [100, 100, 200, 100]);
let other = s.execute("shape.insert", &json!({"preset": "rect", "rect": [400, 300, 50, 50]})).unwrap()["id"].clone();
s.execute("edit.select", &json!({"ids": [pic, other]})).unwrap();
s.execute("arrange.group", &json!({})).unwrap();
// A grouped picture that keeps no box of its own is drawn where the default box puts it.
let sz = s.doc().unwrap().doc.slide_size;
s.edit(|doc, sel| {
fn clear(shapes: &mut [deckcraft_model::Shape], id: &serde_json::Value) {
for sh in shapes {
if json!(sh.id) == *id {
sh.xfrm = None;
}
if let deckcraft_model::ShapeKind::Group { children, .. } = &mut sh.kind {
clear(children, id);
}
}
}
clear(crate::shapes_mut(doc, sel).unwrap(), &pic);
Ok(())
})
.unwrap();
// Left tenth of the default box (10%, 10%, 80% x 20% of the slide): column 0.
let at = [sz.width * 0.1 + 1.0, sz.height * 0.2];
s.execute("picture.transparentColor", &json!({"ids": [pic], "at": at})).unwrap();
assert_eq!(clear_color_of(&s, &pic), Some(deckcraft_color::Rgba::rgb(0, 0, 0)));
let at = [sz.width * 0.9 - 1.0, sz.height * 0.2];
s.execute("picture.transparentColor", &json!({"ids": [pic], "at": at})).unwrap();
assert_eq!(clear_color_of(&s, &pic), Some(deckcraft_color::Rgba::rgb(180, 0, 0)));
}
#[test]
fn transparent_color_at_a_point_takes_the_topmost_selected_picture() {
let mut s = session();
// `under` is inserted first (drawn below), `over` after it (drawn on top); they overlap in x 150..300.
let under = ramp_picture(&mut s, 10, [100, 100, 200, 100]);
let over = ramp_picture(&mut s, 10, [150, 100, 200, 100]);
// Selected bottom first: selection order is the opposite of the stacking order.
s.execute("edit.select", &json!({"ids": [under, over]})).unwrap();
// x = 250 is column 7 of `under` (140) and column 5 of `over` (100): the click is on `over`.
s.execute("picture.transparentColor", &json!({"at": [250, 150]})).unwrap();
assert_eq!(clear_color_of(&s, &over), Some(deckcraft_color::Rgba::rgb(100, 0, 0)));
assert_eq!(clear_color_of(&s, &under), None);
// Where `over` is not, the picture below is still found.
s.execute("picture.transparentColor", &json!({"at": [120, 150]})).unwrap();
assert_eq!(clear_color_of(&s, &under), Some(deckcraft_color::Rgba::rgb(20, 0, 0)));
}
#[test]
fn select_all_slides_scope() {
let mut s = session();
+11 -9
View File
@@ -600,6 +600,16 @@ pub fn parse_path(d: &str) -> BezPath {
bp
}
/// The transform a 3-D camera applies to shape `s` of size `w` x `h` (approximated by the affine
/// map that fits it best), as drawn; `None` for no camera, and for groups, tables and charts.
pub fn camera_turn(rctx: &Ctx, s: &Shape, w: f64, h: f64) -> Option<Affine> {
if matches!(s.kind, ShapeKind::Group { .. } | ShapeKind::Table(_) | ShapeKind::Chart(_)) {
return None;
}
let (e, _) = resolve::effects(rctx, s);
e.as_ref().and_then(|e| e.raw3d.as_deref()).and_then(camera::from_raw).and_then(|c| camera::affine(&c, w, h))
}
/// The rectangle a picture's image occupies given its crop (fractions; negative = padding).
pub fn crop_dest(bounds: Rect, crop: [f64; 4]) -> Rect {
let [l, t, r, b] = crop.map(|v| if v.is_finite() { v.clamp(-10.0, 0.99) } else { 0.0 });
@@ -873,15 +883,7 @@ impl Renderer {
* Affine::translate(-c.to_vec2());
m = parent * Affine::translate(st.offset) * x.affine() * around;
}
// A 3-D camera turns the shape (approximated by the affine transform that fits it best).
if !matches!(s.kind, ShapeKind::Group { .. } | ShapeKind::Table(_) | ShapeKind::Chart(_))
&& let Some(a) = resolve::effects(rctx, s)
.0
.as_ref()
.and_then(|e| e.raw3d.as_deref())
.and_then(camera::from_raw)
.and_then(|c| camera::affine(&c, x.w, x.h))
{
if let Some(a) = camera_turn(rctx, s, x.w, x.h) {
m *= a;
}
let layered = st.opacity < 0.999 || st.clip.is_some();
+48 -14
View File
@@ -5,6 +5,9 @@
//! therefore hands its colour back through the same menu: the menu closes while the pointer picks,
//! then opens again for one frame and `color_grid` returns the colour as if its swatch had been
//! clicked, so every caller applies it its own way.
//!
//! Picture Format ▸ Color ▸ Set Transparent Color uses the same pointer, as PowerPoint does: the
//! click names the colour of the selected picture under it, which turns transparent.
use egui::{Color32, Context, CursorIcon, Id, Pos2, Rect, Stroke, StrokeKind, Ui, pos2, vec2};
@@ -13,15 +16,25 @@ use crate::canvas::Xf;
use crate::theme::{self, Tokens};
const REQUEST: &str = "deckcraft.eyedropper.request";
const TRANSPARENT: &str = "deckcraft.eyedropper.transparent";
const MENU: &str = "deckcraft.eyedropper.menu";
const RESULT: &str = "deckcraft.eyedropper.result";
/// Pixels per slide unit of the picture the Eyedropper looks at.
const SCALE: f64 = 2.0;
/// An Eyedropper waiting for a click: the slide as it is drawn and the menu button it came from.
/// What a click with the pointer does.
#[derive(Clone, Copy)]
enum Pick {
/// Hands the colour to the colour menu on this button.
Menu(Id),
/// Makes the colour of the selected picture under the pointer transparent.
TransparentColor,
}
/// An Eyedropper waiting for a click: the slide as it is drawn and what the click is for.
pub struct State {
menu: Id,
pick: Pick,
slide: usize,
image: deckcraft_render::Image,
}
@@ -50,6 +63,11 @@ pub fn request(ctx: &Context) {
ctx.data_mut(|d| d.insert_temp(key(REQUEST), true));
}
/// Picture Format ▸ Color ▸ "Set Transparent Color" was chosen.
pub fn request_transparent_color(ctx: &Context) {
ctx.data_mut(|d| d.insert_temp(key(TRANSPARENT), true));
}
/// Has the Eyedropper taken a colour for the menu on `button`?
pub fn result_waits_for(ctx: &Context, button: Id) -> bool {
ctx.data(|d| d.get_temp::<Option<(Id, String)>>(key(RESULT))).flatten().is_some_and(|(b, _)| b == button)
@@ -67,8 +85,13 @@ pub fn take_result(ctx: &Context) -> Option<String> {
/// Once a frame: start the Eyedropper when a menu asked for it, stop it when something else is
/// clicked.
pub fn tick(app: &mut SlideApp, ctx: &Context) {
if ctx.data_mut(|d| d.remove_temp::<bool>(key(REQUEST))).unwrap_or(false) {
begin(app, ctx);
if ctx.data_mut(|d| d.remove_temp::<bool>(key(REQUEST))).unwrap_or(false)
&& let Some(menu) = open_menu(ctx)
{
begin(app, Pick::Menu(menu));
}
if ctx.data_mut(|d| d.remove_temp::<bool>(key(TRANSPARENT))).unwrap_or(false) {
begin(app, Pick::TransparentColor);
}
if app.eyedropper.is_some() {
let outside =
@@ -79,8 +102,7 @@ pub fn tick(app: &mut SlideApp, ctx: &Context) {
}
}
fn begin(app: &mut SlideApp, ctx: &Context) {
let Some(menu) = open_menu(ctx) else { return };
fn begin(app: &mut SlideApp, pick: Pick) {
let Some(d) = app.session.active() else { return };
if d.selection.target != deckcraft_engine::Target::Slides {
app.set_status("The Eyedropper takes colours from a slide");
@@ -88,8 +110,11 @@ fn begin(app: &mut SlideApp, ctx: &Context) {
}
let slide = d.selection.slide;
let image = deckcraft_render::render_slide(&d.doc, slide, &deckcraft_render::RenderOpts { scale: SCALE, ..Default::default() });
app.eyedropper = Some(State { menu, slide, image });
app.set_status("Eyedropper: click the slide (Esc cancels)");
app.eyedropper = Some(State { pick, slide, image });
app.set_status(match pick {
Pick::Menu(_) => "Eyedropper: click the slide (Esc cancels)",
Pick::TransparentColor => "Set Transparent Color: click the colour in the picture (Esc cancels)",
});
}
fn hex(c: [u8; 4]) -> String {
@@ -121,15 +146,24 @@ pub fn canvas(app: &mut SlideApp, ui: &Ui, resp: &egui::Response, xf: Xf) -> boo
loupe(ui, pos, c);
}
if resp.clicked() {
let (menu, slide) = (state.menu, state.slide);
let (pick, slide) = (state.pick, state.slide);
app.eyedropper = None;
match app.session.execute("slide.colorAt", &serde_json::json!({"slide": slide, "x": p.x, "y": p.y, "scale": SCALE})) {
Ok(v) => {
if let Some(color) = v["color"].as_str() {
store_result(ui.ctx(), menu, color);
match pick {
Pick::Menu(menu) => {
match app.session.execute("slide.colorAt", &serde_json::json!({"slide": slide, "x": p.x, "y": p.y, "scale": SCALE})) {
Ok(v) => {
if let Some(color) = v["color"].as_str() {
store_result(ui.ctx(), menu, color);
}
}
Err(e) => app.set_status(e.to_string()),
}
}
Pick::TransparentColor => {
if let Err(e) = app.session.execute("picture.transparentColor", &serde_json::json!({"at": [p.x, p.y]})) {
app.set_status(e.to_string());
}
}
Err(e) => app.set_status(e.to_string()),
}
ui.ctx().request_repaint();
}
+39
View File
@@ -4,6 +4,7 @@
use deckcraft_engine::Session;
use egui::{Event, Id, Key, Modifiers, PointerButton, Pos2};
use egui_kittest::Harness;
use egui_kittest::kittest::Queryable;
use serde_json::json;
use crate::eyedropper::{note_open_menu, request, result_waits_for, take_result};
@@ -111,3 +112,41 @@ fn escape_and_a_click_off_the_slide_take_nothing() {
other.run_steps(3);
assert!(other.state().eyedropper.is_none());
}
#[test]
fn set_transparent_color_takes_the_colour_clicked_in_the_picture() {
// Red on the left half, blue on the right.
let pixels = (0..8).flat_map(|i| if i % 4 < 2 { [255, 0, 0, 255] } else { [0, 0, 255, 255] }).collect();
let png = deckcraft_render::Image { width: 4, height: 2, pixels }.to_png();
let mut session = Session::with_new();
session.execute("slide.new", &json!({"layout": "blank"})).unwrap();
let data = deckcraft_engine::cmd::base64_encode(&png);
session.execute("insert.picture", &json!({"data": data, "name": "a.png", "rect": [100, 100, 400, 200]})).unwrap();
let mut app = SlideApp::new(session, Services::default());
app.ui.tab = "Picture Format".into();
let mut h = Harness::builder().with_size(egui::vec2(1600.0, 1200.0)).build_ui_state(
|ui, app: &mut SlideApp| {
app.logic(ui.ctx());
app.ui(ui);
},
app,
);
h.run_steps(4);
// A colour menu opened earlier (Shape Fill, say) must not receive the colour.
note_open_menu(&h.ctx, menu());
h.get_by_label("Color").click();
h.run_steps(3);
h.get_by_label("Set Transparent Color").click();
h.run_steps(3);
assert!(h.state().eyedropper.is_some(), "the command arms the pointer");
let blue_half = at(&h, 450.0, 200.0);
click(&mut h, blue_half);
assert!(h.state().eyedropper.is_none(), "one click ends it");
assert!(!result_waits_for(&h.ctx, menu()), "the colour went to the picture, not to another menu");
let doc = h.state().session.doc().unwrap();
let clear = doc.shapes().iter().find_map(|sh| match &sh.kind {
deckcraft_model::ShapeKind::Picture { fill } => Some(fill.adjust.clear_color),
_ => None,
});
assert_eq!(clear, Some(Some(deckcraft_color::Rgba::rgb(0, 0, 255))));
}
+6 -12
View File
@@ -2652,18 +2652,12 @@ fn picture_format(app: &mut SlideApp, ui: &mut Ui) {
run(app, "picture.recolor", json!({"preset": id}));
}
ui.separator();
ui.menu_button("Set Transparent Color", |ui| {
ui.label("Pixels of this colour become transparent:");
if let Some(c) = widgets::color_grid(ui, &sc, Some("None")) {
match c {
Some(c) => {
let rgb = c.resolve(&sc, None);
run(app, "picture.transparentColor", json!({"color": format!("#{}", rgb.hex())}));
}
None => run(app, "picture.transparentColor", json!({"none": true})),
}
}
});
// As in PowerPoint, the pointer picks the colour in the picture itself: a colour
// chosen from a grid almost never matches a photo's pixels.
if ui.button("Set Transparent Color").on_hover_text("Click a colour in the picture to make it transparent").clicked() {
crate::eyedropper::request_transparent_color(ui.ctx());
ui.close();
}
});
let r = big_button(ui, Icon::ArtisticEffects, "Artistic\nEffects", enabled(app, "picture.artisticEffect"));
egui::Popup::menu(&r).show(|ui| {