mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-30 21:47:00 -06:00
Merge pull request #1449
Format Shape Effects: preset tiles for Shadow, Glow and Soft Edges, shadow Size
This commit is contained in:
@@ -121,7 +121,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Shape Effects",
|
||||
["Shape Format", "Shape Styles"],
|
||||
None,
|
||||
"{shadow?: none|outer|inner|perspective|{color, blur, dist, dir}, glow?: none|{color, radius}, softEdges?: pt, reflection?: none|tight|half|full, bevel?: none|circle|…, threeD?: {camera?: preset, rotX?, rotY?, rotZ?: deg, z?: pt, bevelTop?, bevelBottom?: none|circle|…, bevelTopW?, bevelTopH?, bevelBottomW?, bevelBottomH?, depth?, contour?: pt, material?, lighting?, reset?: true}, shadowProps?: {color?, transparency?: 0..1, blur?: pt, dist?: pt, dir?: deg}, glowProps?: {color?, transparency?, radius?: pt}, reflectionProps?: {transparency?, size?: 0..1, dist?: pt, blur?: pt}, reset?, ids?} (the *Props keys adjust the current effect, adding a default one when there is none)",
|
||||
"{shadow?: none|outer|inner|perspective|{color, blur, dist, dir}, glow?: none|{color, radius}, softEdges?: pt, reflection?: none|tight|half|full, bevel?: none|circle|…, threeD?: {camera?: preset, rotX?, rotY?, rotZ?: deg, z?: pt, bevelTop?, bevelBottom?: none|circle|…, bevelTopW?, bevelTopH?, bevelBottomW?, bevelBottomH?, depth?, contour?: pt, material?, lighting?, reset?: true}, shadowProps?: {color?, transparency?: 0..1, size?: 0.01..2 (1 = 100%), blur?: pt, dist?: pt, dir?: deg}, glowProps?: {color?, transparency?, radius?: pt}, reflectionProps?: {transparency?, size?: 0..1, dist?: pt, blur?: pt}, reset?, ids?} (the *Props keys adjust the current effect, adding a default one when there is none)",
|
||||
has_selection,
|
||||
effects
|
||||
),
|
||||
|
||||
@@ -437,6 +437,12 @@ pub(super) fn effects(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if let Some(d) = num(v, "dir") {
|
||||
x.dir = d.rem_euclid(360.0);
|
||||
}
|
||||
// PowerPoint's Size: 0–200%, both axes; a zero scale would collapse the shadow's matrix.
|
||||
if let Some(z) = num(v, "size") {
|
||||
let z = z.clamp(0.01, 2.0);
|
||||
x.sx = z;
|
||||
x.sy = z;
|
||||
}
|
||||
*slot = Some(x);
|
||||
}
|
||||
if let Some(v) = &glow_props {
|
||||
|
||||
@@ -2328,6 +2328,15 @@ fn shape_effects_props_adjust_shadow_glow_and_reflection() {
|
||||
let f = fx(&s);
|
||||
assert!(f.outer_shadow.is_none());
|
||||
assert_eq!(f.inner_shadow.unwrap().blur, 3.0);
|
||||
// Size scales both axes (PowerPoint's 0–200%); out-of-range values clamp.
|
||||
s.execute("shape.effects", &json!({"shadow": "inner"})).unwrap();
|
||||
s.execute("shape.effects", &json!({"shadowProps": {"size": 1.5}})).unwrap();
|
||||
let sw = fx(&s).inner_shadow.unwrap();
|
||||
assert_eq!((sw.sx, sw.sy), (1.5, 1.5));
|
||||
s.execute("shape.effects", &json!({"shadowProps": {"size": 9}})).unwrap();
|
||||
assert_eq!(fx(&s).inner_shadow.unwrap().sx, 2.0);
|
||||
s.execute("shape.effects", &json!({"shadowProps": {"size": 0}})).unwrap();
|
||||
assert_eq!(fx(&s).inner_shadow.unwrap().sy, 0.01);
|
||||
// Glow: size, colour, transparency; size 0 removes it.
|
||||
s.execute("shape.effects", &json!({"glowProps": {"radius": 12, "transparency": 0.5}})).unwrap();
|
||||
let g = fx(&s).glow.unwrap();
|
||||
|
||||
@@ -14,6 +14,7 @@ mod animation;
|
||||
mod background;
|
||||
mod comments;
|
||||
mod designer;
|
||||
mod effect_presets;
|
||||
mod format_shape;
|
||||
mod smartart;
|
||||
mod thesaurus;
|
||||
@@ -121,6 +122,9 @@ pub mod thumbs;
|
||||
#[cfg(test)]
|
||||
mod background_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod effect_presets_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod pattern_fill_tests;
|
||||
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
//! Format Shape ▸ Effects: preset tiles that draw what each Shadow, Glow and Soft Edges preset does
|
||||
//! to a small card, so the choice is made by looking rather than by reading names.
|
||||
|
||||
use deckcraft_model::style::Effects;
|
||||
use egui::{Color32, CornerRadius, Painter, Rect, Role, Sense, Stroke, StrokeKind, Ui, WidgetInfo, pos2, vec2};
|
||||
|
||||
use crate::theme::Tokens;
|
||||
|
||||
const TILE: egui::Vec2 = vec2(46.0, 38.0);
|
||||
const SHADOWS: [(&str, &str); 6] = [
|
||||
("none", "No shadow"),
|
||||
("outer", "Outer shadow"),
|
||||
("bottom", "Bottom shadow"),
|
||||
("center", "Centered shadow"),
|
||||
("inner", "Inner shadow"),
|
||||
("perspective", "Perspective shadow"),
|
||||
];
|
||||
/// The glow radii (pt) the ribbon's Glow menu offers.
|
||||
pub(super) const GLOWS: [f64; 4] = [5.0, 8.0, 11.0, 18.0];
|
||||
/// The soft-edge radii (pt) the ribbon's Soft Edges menu offers; 0 is "none".
|
||||
pub(super) const SOFT_EDGES: [f64; 6] = [0.0, 1.0, 2.5, 5.0, 10.0, 25.0];
|
||||
|
||||
/// The `shape.effects` shadow preset id that `fx`'s current shadow came from.
|
||||
pub(super) fn shadow_preset_id(fx: &Effects) -> &'static str {
|
||||
if fx.inner_shadow.is_some() {
|
||||
return "inner";
|
||||
}
|
||||
match &fx.outer_shadow {
|
||||
None => "none",
|
||||
Some(s) if s.align == "bl" => "perspective",
|
||||
Some(s) if s.dist == 0.0 => "center",
|
||||
Some(s) if s.dir == 90.0 => "bottom",
|
||||
Some(_) => "outer",
|
||||
}
|
||||
}
|
||||
|
||||
/// A row of shadow preset tiles; returns the id of the one clicked.
|
||||
pub(super) fn shadow_row(ui: &mut Ui, current: &str) -> Option<&'static str> {
|
||||
let mut picked = None;
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
for (id, label) in SHADOWS {
|
||||
if tile(ui, label, id == current, |p, r| paint_shadow(p, r, id)).clicked() {
|
||||
picked = Some(id);
|
||||
}
|
||||
}
|
||||
});
|
||||
picked
|
||||
}
|
||||
|
||||
/// A row of glow tiles (the first clears it); returns the radius clicked, 0 for none.
|
||||
pub(super) fn glow_row(ui: &mut Ui, current: f64) -> Option<f64> {
|
||||
let mut picked = None;
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
for r in std::iter::once(0.0).chain(GLOWS) {
|
||||
let label = if r == 0.0 { "No glow".to_string() } else { format!("{r} pt glow") };
|
||||
if tile(ui, &label, r == current, |p, rect| paint_glow(p, rect, r)).clicked() {
|
||||
picked = Some(r);
|
||||
}
|
||||
}
|
||||
});
|
||||
picked
|
||||
}
|
||||
|
||||
/// A row of soft-edge tiles (the first clears it); returns the radius clicked, 0 for none.
|
||||
pub(super) fn soft_edge_row(ui: &mut Ui, current: f64) -> Option<f64> {
|
||||
let mut picked = None;
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
for r in SOFT_EDGES {
|
||||
let label = if r == 0.0 { "No soft edges".to_string() } else { format!("{r} pt soft edges") };
|
||||
if tile(ui, &label, r == current, |p, rect| paint_soft(p, rect, r)).clicked() {
|
||||
picked = Some(r);
|
||||
}
|
||||
}
|
||||
});
|
||||
picked
|
||||
}
|
||||
|
||||
fn tile(ui: &mut Ui, label: &str, selected: bool, paint: impl FnOnce(&Painter, Rect)) -> egui::Response {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let (rect, resp) = ui.allocate_exact_size(TILE, Sense::click());
|
||||
resp.widget_info(|| WidgetInfo::selected(Role::Button, true, selected, label));
|
||||
let p = ui.painter();
|
||||
let round = CornerRadius::same(5);
|
||||
p.rect_filled(rect, round, if resp.hovered() { t.hover } else { t.field_bg });
|
||||
paint(&p.with_clip_rect(rect.shrink(1.0)), rect);
|
||||
let stroke = if selected { Stroke::new(2.0, t.accent) } else { Stroke::new(1.0, t.border) };
|
||||
p.rect_stroke(rect, round, stroke, StrokeKind::Inside);
|
||||
resp.on_hover_text(label)
|
||||
}
|
||||
|
||||
/// The sample card every preview draws on, centred in `tile`.
|
||||
fn card(tile: Rect) -> Rect {
|
||||
Rect::from_center_size(tile.center(), vec2(22.0, 16.0))
|
||||
}
|
||||
|
||||
fn card_fill(p: &Painter, r: Rect) {
|
||||
p.rect_filled(r, CornerRadius::same(2), Tokens::get(p.ctx()).accent);
|
||||
}
|
||||
|
||||
/// `layers` rounded rects growing from `r` by up to `spread`, each faint, so the edge falls off.
|
||||
fn halo(p: &Painter, r: Rect, spread: f32, color: Color32, layers: u32) {
|
||||
for i in (1..=layers).rev() {
|
||||
let k = i as f32 / layers as f32;
|
||||
let a = (f32::from(color.a()) * (1.0 - k * 0.8) / layers as f32 * 2.0).min(255.0) as u8;
|
||||
p.rect_filled(
|
||||
r.expand(spread * k),
|
||||
CornerRadius::same((2.0 + spread * k) as u8),
|
||||
Color32::from_rgba_unmultiplied(color.r(), color.g(), color.b(), a),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_shadow(p: &Painter, tile: Rect, id: &str) {
|
||||
let r = card(tile);
|
||||
let ink = Color32::from_rgba_unmultiplied(0, 0, 0, 120);
|
||||
match id {
|
||||
"outer" => halo(p, r.translate(vec2(3.0, 3.0)), 3.0, ink, 4),
|
||||
"bottom" => halo(p, r.translate(vec2(0.0, 4.0)), 3.0, ink, 4),
|
||||
"center" => halo(p, r, 5.0, ink, 5),
|
||||
"perspective" => {
|
||||
let base = r.max.y + 1.0;
|
||||
let slant = [pos2(r.min.x - 6.0, base), pos2(r.max.x - 8.0, base), pos2(r.max.x - 2.0, base + 5.0), pos2(r.min.x - 12.0, base + 5.0)];
|
||||
p.add(egui::Shape::convex_polygon(slant.to_vec(), Color32::from_rgba_unmultiplied(0, 0, 0, 90), Stroke::NONE));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
card_fill(p, r);
|
||||
if id == "inner" {
|
||||
for i in 0..4 {
|
||||
let a = 110 - i * 25;
|
||||
p.rect_stroke(
|
||||
r.shrink(i as f32),
|
||||
CornerRadius::same(2),
|
||||
Stroke::new(1.0, Color32::from_rgba_unmultiplied(0, 0, 0, a as u8)),
|
||||
StrokeKind::Inside,
|
||||
);
|
||||
}
|
||||
}
|
||||
if id == "none" {
|
||||
p.line_segment([tile.left_bottom() + vec2(6.0, -6.0), tile.right_top() + vec2(-6.0, 6.0)], Stroke::new(1.5, Tokens::get(p.ctx()).text_faint));
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_glow(p: &Painter, tile: Rect, radius: f64) {
|
||||
let r = card(tile);
|
||||
if radius > 0.0 {
|
||||
// 18 pt fills the tile's margin; smaller radii scale down.
|
||||
halo(p, r, (radius as f32 / 18.0 * 9.0).max(2.0), Tokens::get(p.ctx()).accent.gamma_multiply(0.9), 6);
|
||||
}
|
||||
card_fill(p, r);
|
||||
if radius == 0.0 {
|
||||
p.line_segment([tile.left_bottom() + vec2(6.0, -6.0), tile.right_top() + vec2(-6.0, 6.0)], Stroke::new(1.5, Tokens::get(p.ctx()).text_faint));
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_soft(p: &Painter, tile: Rect, radius: f64) {
|
||||
let t = Tokens::get(p.ctx());
|
||||
let r = card(tile).expand(2.0);
|
||||
card_fill(p, r);
|
||||
// Rings of the tile's own background, opaque at the edge and clear inward, feather the rim.
|
||||
let depth = (radius as f32 / 25.0 * 7.0).round().max(if radius > 0.0 { 1.0 } else { 0.0 }) as u32;
|
||||
let bg = t.field_bg;
|
||||
for i in 0..depth {
|
||||
let a = (255.0 * (1.0 - i as f32 / depth as f32)) as u8;
|
||||
let c = Color32::from_rgba_unmultiplied(bg.r(), bg.g(), bg.b(), a);
|
||||
p.rect_stroke(r.shrink(i as f32), CornerRadius::same(2), Stroke::new(1.0, c), StrokeKind::Inside);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use deckcraft_model::style::Shadow;
|
||||
use deckcraft_model::{ColorRef, SchemeSlot};
|
||||
|
||||
fn shadow(dist: f64, dir: f64, align: &str, inner: bool) -> Shadow {
|
||||
Shadow {
|
||||
color: ColorRef::scheme(SchemeSlot::Dk1),
|
||||
blur: 4.0,
|
||||
dist,
|
||||
dir,
|
||||
inner,
|
||||
sx: 1.0,
|
||||
sy: 1.0,
|
||||
kx: 0.0,
|
||||
ky: 0.0,
|
||||
align: align.into(),
|
||||
rotate_with_shape: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shadow_preset_id_names_each_preset() {
|
||||
let with = |o: Option<Shadow>, i: Option<Shadow>| Effects { outer_shadow: o, inner_shadow: i, ..Effects::default() };
|
||||
assert_eq!(shadow_preset_id(&with(None, None)), "none");
|
||||
assert_eq!(shadow_preset_id(&with(Some(shadow(3.0, 45.0, "tl", false)), None)), "outer");
|
||||
assert_eq!(shadow_preset_id(&with(Some(shadow(3.0, 90.0, "tl", false)), None)), "bottom");
|
||||
assert_eq!(shadow_preset_id(&with(Some(shadow(0.0, 45.0, "tl", false)), None)), "center");
|
||||
assert_eq!(shadow_preset_id(&with(Some(shadow(0.0, 90.0, "bl", false)), None)), "perspective");
|
||||
assert_eq!(shadow_preset_id(&with(None, Some(shadow(3.0, 45.0, "tl", true)))), "inner");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
//! Format Shape ▸ Effects: the Shadow, Glow and Soft Edges preset tiles.
|
||||
|
||||
use deckcraft_engine::Session;
|
||||
use deckcraft_model::style::Effects;
|
||||
use egui::accesskit::Toggled;
|
||||
use egui_kittest::Harness;
|
||||
use egui_kittest::kittest::{NodeT, Queryable};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{Services, SlideApp};
|
||||
|
||||
fn harness() -> Harness<'static, SlideApp> {
|
||||
let mut s = Session::with_new();
|
||||
let r = s.execute("shape.insert", &json!({"preset": "rect", "rect": [80.0, 80.0, 200.0, 120.0]})).unwrap();
|
||||
let id = r.get("id").and_then(|v| v.as_u64()).unwrap();
|
||||
s.execute("edit.select", &json!({"ids": [id]})).unwrap();
|
||||
let mut app = SlideApp::new(s, Services::default());
|
||||
app.ui.pane = Some("format".into());
|
||||
app.ui.format_tab = "effects".into();
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1800.0, 1100.0)).build_ui_state(
|
||||
|ui, app: &mut SlideApp| {
|
||||
app.logic(ui.ctx());
|
||||
app.ui(ui);
|
||||
},
|
||||
app,
|
||||
);
|
||||
h.run_steps(3);
|
||||
h
|
||||
}
|
||||
|
||||
fn effects(h: &Harness<'_, SlideApp>) -> Effects {
|
||||
h.state().session.active().unwrap().selected_shapes()[0].effects.clone().unwrap_or_default()
|
||||
}
|
||||
|
||||
fn click(h: &mut Harness<'_, SlideApp>, label: &str) {
|
||||
h.get_by_label(label).click();
|
||||
h.run_steps(3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preset_tiles_set_shadow_glow_and_soft_edges() {
|
||||
let mut h = harness();
|
||||
click(&mut h, "Bottom shadow");
|
||||
let sw = effects(&h).outer_shadow.unwrap();
|
||||
assert_eq!((sw.dir, sw.dist), (90.0, 3.0));
|
||||
// The Size slider appears once there is a shadow.
|
||||
assert!(h.query_all_by_label("Size").count() >= 2);
|
||||
|
||||
click(&mut h, "Inner shadow");
|
||||
let fx = effects(&h);
|
||||
assert!(fx.outer_shadow.is_none() && fx.inner_shadow.is_some());
|
||||
click(&mut h, "No shadow");
|
||||
let fx = effects(&h);
|
||||
assert!(fx.outer_shadow.is_none() && fx.inner_shadow.is_none());
|
||||
|
||||
// A radius tile keeps the glow's colour and transparency.
|
||||
h.state_mut().session.execute("shape.effects", &json!({"glowProps": {"color": "accent2", "transparency": 0.35, "radius": 8.0}})).unwrap();
|
||||
h.run_steps(3);
|
||||
let colour = effects(&h).glow.unwrap().color;
|
||||
click(&mut h, "18 pt glow");
|
||||
let glow = effects(&h).glow.unwrap();
|
||||
assert_eq!(glow.radius, 18.0);
|
||||
assert_eq!(glow.color, colour);
|
||||
click(&mut h, "No glow");
|
||||
assert!(effects(&h).glow.is_none());
|
||||
|
||||
click(&mut h, "10 pt soft edges");
|
||||
assert_eq!(effects(&h).soft_edge, Some(10.0));
|
||||
click(&mut h, "No soft edges");
|
||||
assert_eq!(effects(&h).soft_edge, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_current_preset_tile_is_marked_selected() {
|
||||
let mut h = harness();
|
||||
click(&mut h, "5 pt glow");
|
||||
assert_eq!(h.get_by_label("5 pt glow").accesskit_node().toggled(), Some(Toggled::True));
|
||||
assert_eq!(h.get_by_label("11 pt glow").accesskit_node().toggled(), Some(Toggled::False));
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
use super::effect_presets;
|
||||
use super::{Fill, Icon, ShapeKind, SlideApp, Tokens, Ui, color_button, cref_param, json, run, scheme, theme, vec2, widgets};
|
||||
|
||||
/// Percent transparency (0 = opaque) of a solid fill, the same number `shape.fill`'s
|
||||
@@ -47,17 +48,11 @@ fn format_tabs(app: &mut SlideApp, ui: &mut Ui, sh: &deckcraft_model::Shape) {
|
||||
|
||||
fn effects_tab(app: &mut SlideApp, ui: &mut Ui, sh: &deckcraft_model::Shape) {
|
||||
ui.label(egui::RichText::new("Shadow").font(theme::bold(13.0)));
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
for (l, v) in
|
||||
[("None", "none"), ("Outer", "outer"), ("Bottom", "bottom"), ("Center", "center"), ("Inner", "inner"), ("Perspective", "perspective")]
|
||||
{
|
||||
if ui.button(l).clicked() {
|
||||
run(app, "shape.effects", json!({"shadow": v}));
|
||||
}
|
||||
}
|
||||
});
|
||||
let fx = sh.effects.clone().unwrap_or_default();
|
||||
let shadow = fx.outer_shadow.as_ref().or(fx.inner_shadow.as_ref());
|
||||
if let Some(v) = effect_presets::shadow_row(ui, effect_presets::shadow_preset_id(&fx)) {
|
||||
run(app, "shape.effects", json!({"shadow": v}));
|
||||
}
|
||||
let shadow = fx.inner_shadow.as_ref().or(fx.outer_shadow.as_ref());
|
||||
if let Some(sw) = shadow {
|
||||
color_button(app, ui, "Color", None, |c| ("shape.effects".into(), json!({"shadowProps": {"color": c.as_ref().map(cref_param)}})));
|
||||
fx_slider(
|
||||
@@ -72,6 +67,7 @@ fn effects_tab(app: &mut SlideApp, ui: &mut Ui, sh: &deckcraft_model::Shape) {
|
||||
"transparency",
|
||||
0.01,
|
||||
);
|
||||
fx_slider(app, ui, ("shadow", sh.id.0, "s"), "Size", sw.sx * 100.0, 1.0..=200.0, "%", "shadowProps", "size", 0.01);
|
||||
fx_slider(app, ui, ("shadow", sh.id.0, "b"), "Blur", sw.blur, 0.0..=100.0, " pt", "shadowProps", "blur", 1.0);
|
||||
fx_slider(app, ui, ("shadow", sh.id.0, "a"), "Angle", sw.dir, 0.0..=359.0, "°", "shadowProps", "dir", 1.0);
|
||||
fx_slider(app, ui, ("shadow", sh.id.0, "d"), "Distance", sw.dist, 0.0..=200.0, " pt", "shadowProps", "dist", 1.0);
|
||||
@@ -105,6 +101,9 @@ fn effects_tab(app: &mut SlideApp, ui: &mut Ui, sh: &deckcraft_model::Shape) {
|
||||
ui.add_space(6.0);
|
||||
ui.label(egui::RichText::new("Glow").font(theme::bold(13.0)));
|
||||
let radius = fx.glow.as_ref().map_or(0.0, |g| g.radius);
|
||||
if let Some(r) = effect_presets::glow_row(ui, radius) {
|
||||
run(app, "shape.effects", json!({"glowProps": {"radius": r}}));
|
||||
}
|
||||
fx_slider(app, ui, ("glow", sh.id.0, "r"), "Size", radius, 0.0..=150.0, " pt", "glowProps", "radius", 1.0);
|
||||
if let Some(g) = &fx.glow {
|
||||
color_button(app, ui, "Color", None, |c| ("shape.effects".into(), json!({"glowProps": {"color": c.as_ref().map(cref_param)}})));
|
||||
@@ -123,6 +122,9 @@ fn effects_tab(app: &mut SlideApp, ui: &mut Ui, sh: &deckcraft_model::Shape) {
|
||||
}
|
||||
ui.label(egui::RichText::new("Soft Edges").font(theme::bold(13.0)));
|
||||
let mut s = sh.effects.as_ref().and_then(|e| e.soft_edge).unwrap_or(0.0);
|
||||
if let Some(r) = effect_presets::soft_edge_row(ui, s) {
|
||||
run(app, "shape.effects", json!({"softEdges": r}));
|
||||
}
|
||||
if ui.add(egui::Slider::new(&mut s, 0.0..=50.0).text("size (pt)")).drag_stopped() {
|
||||
run(app, "shape.effects", json!({"softEdges": s}));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user