M3.24: basic appearance, Layers target dots, Mixed Appearances and Clear resets opacity
An appearance is basic with at most one fill and one stroke, none hidden or with its own opacity, blend mode or effects, and no object effects; Reduce to Basic keeps the topmost visible fill and stroke. The Layers panel fills an object's target circle for any non-default transparency (blend mode, isolation, knockout or an opacity mask: Node::has_default_transparency). A multi-selection whose objects differ shows "Mixed Appearances" with no rows. The Appearance panel's object row and the Control bar name the selection from one source, and type objects are "Type". Expand Appearance is only in the Object menu. Clear Appearance also resets the object's opacity and blend mode. Tests cover addFill/addStroke, setItem/removeItem, clear, reduceToBasic and graphicStyle.apply. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
b6b87ee9b6
commit
3692b9d233
|
|
@ -326,15 +326,14 @@ impl Appearance {
|
|||
pub fn stroke_width(&self) -> f64 {
|
||||
self.stroke().filter(|s| !s.paint.is_none()).map(|s| s.width).unwrap_or(0.0)
|
||||
}
|
||||
/// Is this the basic one-fill-one-stroke appearance without effects?
|
||||
/// Is this a basic appearance: at most one fill and one stroke, none hidden or with its own
|
||||
/// opacity, blend mode or effects, and no object effects?
|
||||
pub fn is_basic(&self) -> bool {
|
||||
let count = |fill: bool| self.items.iter().filter(|i| i.is_fill() == fill).count();
|
||||
self.effects.is_empty()
|
||||
&& self.items.len() <= 2
|
||||
&& self.items.iter().filter(|i| matches!(i, AppearanceItem::Fill(_))).count() <= 1
|
||||
&& self.items.iter().all(|i| match i {
|
||||
AppearanceItem::Fill(f) => f.effects.is_empty() && f.opacity == 1.0 && f.blend == BlendMode::Normal,
|
||||
AppearanceItem::Stroke(s) => s.effects.is_empty() && s.opacity == 1.0 && s.blend == BlendMode::Normal,
|
||||
})
|
||||
&& count(true) <= 1
|
||||
&& count(false) <= 1
|
||||
&& self.items.iter().all(|i| i.visible() && i.effects().is_empty() && i.opacity() == 1.0 && i.blend() == BlendMode::Normal)
|
||||
}
|
||||
/// Fill item `index`, or the topmost fill for `None`. `None` when that item is not a fill.
|
||||
pub fn fill_at(&self, index: Option<usize>) -> Option<&FillLayer> {
|
||||
|
|
@ -488,6 +487,27 @@ mod tests {
|
|||
assert_eq!((a.items[1].kind_name(), a.items[2].kind_name()), ("stroke", "fill"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_means_one_plain_fill_and_stroke() {
|
||||
let stroke = || AppearanceItem::Stroke(StrokeLayer::new(Paint::solid(Color::BLACK), 1.0));
|
||||
let mut a = Appearance { items: vec![stroke(), stroke()], effects: vec![] };
|
||||
assert!(!a.is_basic(), "two strokes");
|
||||
a.items.pop();
|
||||
assert!(a.is_basic() && Appearance::default().is_basic());
|
||||
let mut b = Appearance::default_art();
|
||||
b.stroke_mut().unwrap().visible = false;
|
||||
assert!(!b.is_basic(), "a hidden stroke");
|
||||
let mut c = Appearance::default_art();
|
||||
c.fill_mut().unwrap().opacity = 0.5;
|
||||
assert!(!c.is_basic(), "fill opacity");
|
||||
let mut d = Appearance::default_art();
|
||||
d.items.push(AppearanceItem::Fill(FillLayer::new(Paint::None)));
|
||||
assert!(!d.is_basic(), "two fills");
|
||||
let mut e = Appearance::default_art();
|
||||
e.effects.push(Effect { id: "distort.roughen".into(), params: serde_json::Value::Null, visible: true });
|
||||
assert!(!e.is_basic(), "an effect");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_creates_missing() {
|
||||
let mut a = Appearance::default();
|
||||
|
|
|
|||
|
|
@ -357,6 +357,11 @@ impl Node {
|
|||
pub fn is_container(&self) -> bool {
|
||||
self.children().is_some()
|
||||
}
|
||||
/// Full opacity, Normal blending, no isolation, knockout or opacity mask (the Layers panel
|
||||
/// fills an object's target circle otherwise).
|
||||
pub fn has_default_transparency(&self) -> bool {
|
||||
self.opacity >= 1.0 && self.blend == BlendMode::Normal && !self.isolate && !self.knockout && self.mask.is_none()
|
||||
}
|
||||
/// Kind name as the Layers panel / Properties panel shows it.
|
||||
pub fn kind_label(&self) -> &'static str {
|
||||
match &self.kind {
|
||||
|
|
@ -367,7 +372,7 @@ impl Node {
|
|||
NodeKind::Path { guide: true, .. } => "Guide",
|
||||
NodeKind::Path { .. } => "Path",
|
||||
NodeKind::Compound { .. } => "Compound Path",
|
||||
NodeKind::Text(_) => "Text",
|
||||
NodeKind::Text(_) => "Type",
|
||||
NodeKind::Image(_) => "Image",
|
||||
NodeKind::SymbolInstance { .. } => "Symbol",
|
||||
NodeKind::Blend { .. } => "Blend",
|
||||
|
|
@ -545,6 +550,24 @@ mod tests {
|
|||
assert_eq!(l.label(), "Rectangle");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_transparency() {
|
||||
let p = || Node::path(NodeId(1), shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)), Appearance::default_art());
|
||||
assert!(p().has_default_transparency());
|
||||
let tweaks: [fn(&mut Node); 5] = [
|
||||
|n| n.opacity = 0.5,
|
||||
|n| n.blend = BlendMode::Multiply,
|
||||
|n| n.isolate = true,
|
||||
|n| n.knockout = true,
|
||||
|n| n.mask = Some(Box::new(OpacityMask::new(n.clone(), true))),
|
||||
];
|
||||
for tweak in tweaks {
|
||||
let mut n = p();
|
||||
tweak(&mut n);
|
||||
assert!(!n.has_default_transparency(), "{n:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transform_group_recurses() {
|
||||
let p = Node::path(NodeId(2), shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)), Appearance::default_art());
|
||||
|
|
|
|||
|
|
@ -38,8 +38,24 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
has_selection,
|
||||
|s, p| add_item(s, p, false)
|
||||
),
|
||||
cmd!("appearance.clear", "Clear Appearance", ["Window", "Appearance"], None, "{ids?}", has_selection, clear_appearance),
|
||||
cmd!("appearance.reduceToBasic", "Reduce to Basic Appearance", ["Window", "Appearance"], None, "{ids?}", has_selection, reduce_basic),
|
||||
cmd!(
|
||||
"appearance.clear",
|
||||
"Clear Appearance",
|
||||
["Window", "Appearance"],
|
||||
None,
|
||||
"{ids?} leave each object one empty fill and stroke (None) and reset its opacity and blend mode",
|
||||
has_selection,
|
||||
clear_appearance
|
||||
),
|
||||
cmd!(
|
||||
"appearance.reduceToBasic",
|
||||
"Reduce to Basic Appearance",
|
||||
["Window", "Appearance"],
|
||||
None,
|
||||
"{ids?} keep only the topmost visible fill and stroke, without their own opacity, blend mode or effects, and drop the object's effects",
|
||||
has_selection,
|
||||
reduce_basic
|
||||
),
|
||||
cmd!(
|
||||
"appearance.setItem",
|
||||
"Appearance Item",
|
||||
|
|
@ -261,7 +277,10 @@ fn clear_appearance(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
s.edit("Clear Appearance", |d, _| {
|
||||
for id in &ids {
|
||||
if let Some(n) = d.node_mut(*id) {
|
||||
// No fill, no stroke, and the object's Opacity row back to Default.
|
||||
n.appearance = Appearance::basic(Paint::None, Paint::None, 1.0);
|
||||
n.opacity = 1.0;
|
||||
n.blend = BlendMode::Normal;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
|
@ -275,10 +294,12 @@ fn reduce_basic(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
s.edit("Reduce to Basic Appearance", |d, _| {
|
||||
for id in &ids {
|
||||
if let Some(n) = d.node_mut(*id) {
|
||||
let f = n.appearance.fill_paint();
|
||||
let st = n.appearance.stroke().cloned();
|
||||
n.appearance = Appearance { items: vec![AppearanceItem::Fill(FillLayer::new(f))], effects: vec![] };
|
||||
if let Some(mut st) = st {
|
||||
// The topmost visible fill and stroke stay, without their own transparency or effects.
|
||||
let top = |fill: bool| n.appearance.items.iter().rev().find(|i| i.visible() && i.is_fill() == fill).cloned();
|
||||
let fill = top(true).map_or(Paint::None, |f| f.paint().clone());
|
||||
let stroke = top(false);
|
||||
n.appearance = Appearance { items: vec![AppearanceItem::Fill(FillLayer::new(fill))], effects: vec![] };
|
||||
if let Some(AppearanceItem::Stroke(mut st)) = stroke {
|
||||
st.effects.clear();
|
||||
st.opacity = 1.0;
|
||||
st.blend = BlendMode::Normal;
|
||||
|
|
|
|||
|
|
@ -267,3 +267,77 @@ fn text_fill_items_composite_with_their_opacity() {
|
|||
let px = render(&s).pixel(x, y);
|
||||
assert!((110..=145).contains(&px[0]) && px[1] < 10, "{px:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_fill_and_stroke_copy_the_top_ones() {
|
||||
let mut s = session();
|
||||
let id = rect(&mut s, 100.0);
|
||||
run(&mut s, "paint.setFill", json!({"color": "#ff0000"}));
|
||||
run(&mut s, "stroke.set", json!({"weight": 4}));
|
||||
run(&mut s, "appearance.addFill", json!({}));
|
||||
run(&mut s, "appearance.addStroke", json!({})); // [Fill, Stroke, Fill, Stroke]
|
||||
let n = node(&s, id);
|
||||
assert_eq!(n.appearance.items.iter().map(AppearanceItem::kind_name).collect::<Vec<_>>(), ["fill", "stroke", "fill", "stroke"]);
|
||||
assert_eq!(fill_paint(&n, 2).color().unwrap().to_hex(), "#ff0000");
|
||||
assert_eq!(stroke(&n, 3).width, 4.0);
|
||||
assert!(!n.appearance.is_basic());
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
assert_eq!(node(&s, id).appearance.items.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_item_and_remove_item() {
|
||||
let mut s = session();
|
||||
let id = rect(&mut s, 100.0);
|
||||
run(&mut s, "appearance.setItem", json!({"index": 1, "opacity": 50, "blend": "screen", "weight": 3, "color": "#0000ff"}));
|
||||
let n = node(&s, id);
|
||||
let st = stroke(&n, 1);
|
||||
assert_eq!((st.opacity, st.blend, st.width), (0.5, BlendMode::Screen, 3.0));
|
||||
assert_eq!(st.paint.color().unwrap().to_hex(), "#0000ff");
|
||||
run(&mut s, "appearance.setItem", json!({"index": 0, "visible": false, "opacity": 0.25}));
|
||||
let n = node(&s, id);
|
||||
assert!(!n.appearance.items[0].visible() && n.appearance.items[0].opacity() == 0.25);
|
||||
assert!(s.execute("appearance.setItem", &json!({})).is_err());
|
||||
run(&mut s, "appearance.removeItem", json!({"index": 0}));
|
||||
let n = node(&s, id);
|
||||
assert_eq!(n.appearance.items.len(), 1);
|
||||
assert!(n.appearance.fill().is_none() && n.appearance.stroke().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_and_reduce_to_basic() {
|
||||
let mut s = session();
|
||||
let id = rect(&mut s, 100.0);
|
||||
run(&mut s, "appearance.addStroke", json!({}));
|
||||
run(&mut s, "appearance.setItem", json!({"index": 2, "visible": false, "color": "#00ff00"}));
|
||||
run(&mut s, "appearance.setItem", json!({"index": 1, "opacity": 0.5}));
|
||||
run(&mut s, "effect.apply", json!({"effect": "distort.roughen", "item": 1}));
|
||||
run(&mut s, "effect.apply", json!({"effect": "stylize.dropShadow", "item": null}));
|
||||
run(&mut s, "transparency.set", json!({"opacity": 40, "blend": "multiply"}));
|
||||
// Reduce keeps the topmost visible fill and stroke, plain, and the object's transparency.
|
||||
run(&mut s, "appearance.reduceToBasic", json!({}));
|
||||
let n = node(&s, id);
|
||||
assert!(n.appearance.is_basic(), "{:?}", n.appearance);
|
||||
assert_eq!(stroke(&n, 1).paint, Paint::solid(Color::BLACK));
|
||||
assert_eq!((n.opacity, n.blend), (0.4, BlendMode::Multiply));
|
||||
// Clear leaves one None fill and stroke, and resets the object's Opacity row.
|
||||
run(&mut s, "appearance.clear", json!({}));
|
||||
let n = node(&s, id);
|
||||
assert_eq!(n.appearance, vectorcraft_doc::Appearance::basic(Paint::None, Paint::None, 1.0));
|
||||
assert_eq!((n.opacity, n.blend), (1.0, BlendMode::Normal));
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
assert_eq!(node(&s, id).opacity, 0.4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn graphic_style_apply_replaces_the_stack() {
|
||||
let mut s = session();
|
||||
let a = rect(&mut s, 100.0);
|
||||
run(&mut s, "appearance.addFill", json!({}));
|
||||
run(&mut s, "effect.apply", json!({"effect": "distort.roughen"}));
|
||||
let name = run(&mut s, "graphicStyle.new", json!({"name": "Rough"}))["name"].clone();
|
||||
let b = rect(&mut s, 250.0);
|
||||
run(&mut s, "graphicStyle.apply", json!({ "name": name }));
|
||||
assert_eq!(node(&s, b).appearance, node(&s, a).appearance);
|
||||
assert!(s.execute("graphicStyle.apply", &json!({"name": "nope"})).is_err());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,16 +92,10 @@ pub fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
let units = st.doc.units;
|
||||
let first = sel.first().and_then(|id| st.doc.node(*id)).cloned();
|
||||
let anchor_mode = !st.selection.anchors.is_empty();
|
||||
let label = match (&first, sel.len()) {
|
||||
(None, _) => "No Selection".to_string(),
|
||||
(_, n) if n > 1 => "Mixed Objects".to_string(),
|
||||
(Some(_), _) if anchor_mode => "Anchor Point".to_string(),
|
||||
(Some(n), _) => match &n.kind {
|
||||
NodeKind::Text(_) => "Type".into(),
|
||||
NodeKind::Image(_) => "Embedded".into(),
|
||||
NodeKind::Path { live: Some(_), .. } => n.kind_label().to_string(),
|
||||
_ => n.kind_label().to_string(),
|
||||
},
|
||||
let label = match &first {
|
||||
Some(_) if sel.len() == 1 && anchor_mode => "Anchor Point",
|
||||
Some(n) if sel.len() == 1 && matches!(n.kind, NodeKind::Image(_)) => "Embedded",
|
||||
_ => crate::panels::appearance::object_label(app),
|
||||
};
|
||||
ui.label(egui::RichText::new(label).font(theme::semibold(12.0)).color(t.text));
|
||||
ui.add_space(6.0);
|
||||
|
|
|
|||
|
|
@ -1638,8 +1638,6 @@ fn effect_menu() -> Vec<Item> {
|
|||
for e in cat.iter().filter(|e| !e.menu.last().is_some_and(|m| order.contains(m))) {
|
||||
out.push(Item::Cmd(e.label, "effect.dialog", json!({ "effect": e.id })));
|
||||
}
|
||||
out.push(Sep);
|
||||
out.push(c("Expand Appearance", "effect.expandAppearance"));
|
||||
out
|
||||
}
|
||||
|
||||
|
|
@ -1771,4 +1769,13 @@ mod tests {
|
|||
}
|
||||
assert_eq!(pretty_shortcut(""), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_appearance_is_an_object_menu_item_only() {
|
||||
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({})).unwrap();
|
||||
let paths: Vec<Vec<String>> =
|
||||
menu_entries(&app).into_iter().filter(|e| e.command.as_deref() == Some("effect.expandAppearance")).map(|e| e.path).collect();
|
||||
assert_eq!(paths, [vec!["Object".to_string()]]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -173,18 +173,17 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
select_row(app, ui.ctx(), Sel::None);
|
||||
}
|
||||
ui.painter().line_segment([r.left_bottom(), r.right_bottom()], Stroke::new(1.0, t.input_border));
|
||||
let label = match (&node, super::selection_len(app)) {
|
||||
(None, _) => "No Selection".to_string(),
|
||||
(Some(_), n) if n > 1 => "Mixed Objects".to_string(),
|
||||
(Some(n), _) => n.kind_label().to_string(),
|
||||
};
|
||||
let mixed = mixed_appearances(app);
|
||||
let label = if mixed { "Mixed Appearances" } else { object_label(app) };
|
||||
if !hide_thumb {
|
||||
let th = Rect::from_min_size(r.left_center() + vec2(6.0, -12.0), vec2(24.0, 24.0));
|
||||
let fill = node.as_ref().map(|n| n.appearance.fill_paint()).unwrap_or_else(|| app.session.paint.fill.clone());
|
||||
chip(ui, th, &fill);
|
||||
}
|
||||
text(ui, r.left_center() + vec2(if hide_thumb { 8.0 } else { 46.0 }, 0.0), &label, true);
|
||||
text(ui, r.left_center() + vec2(if hide_thumb { 8.0 } else { 46.0 }, 0.0), label, true);
|
||||
match &node {
|
||||
// Objects that differ have no common stack to list.
|
||||
Some(_) if mixed => {}
|
||||
Some(n) => stack(app, ui, n, sel),
|
||||
None => default_stack(app, ui),
|
||||
}
|
||||
|
|
@ -194,6 +193,25 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
bottom(app, ui, node.as_ref(), sel);
|
||||
}
|
||||
|
||||
/// The selection as the Control bar and the object row name it: "No Selection", "Mixed Objects"
|
||||
/// or the object's kind ("Path", "Type", …).
|
||||
pub(crate) fn object_label(app: &VectorcraftApp) -> &'static str {
|
||||
let Some(st) = app.session.active() else { return "No Selection" };
|
||||
match st.selection.objects.as_slice() {
|
||||
[] => "No Selection",
|
||||
[id] => st.doc.node(*id).map_or("No Selection", Node::kind_label),
|
||||
_ => "Mixed Objects",
|
||||
}
|
||||
}
|
||||
|
||||
/// Do the selected objects differ in appearance (fills, strokes, effects, opacity or blend mode)?
|
||||
fn mixed_appearances(app: &VectorcraftApp) -> bool {
|
||||
let Some(st) = app.session.active() else { return false };
|
||||
let mut nodes = st.selection.objects.iter().filter_map(|id| st.doc.node(*id));
|
||||
let Some(first) = nodes.next() else { return false };
|
||||
nodes.any(|n| n.appearance != first.appearance || n.opacity != first.opacity || n.blend != first.blend)
|
||||
}
|
||||
|
||||
/// The defaults for new art when nothing is selected (read-only rows).
|
||||
fn default_stack(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let p = app.session.paint.clone();
|
||||
|
|
|
|||
|
|
@ -221,7 +221,7 @@ fn row(
|
|||
let col_x = r.right() - 43.5;
|
||||
ui.painter().line_segment([egui::pos2(col_x, r.top()), egui::pos2(col_x, r.bottom())], Stroke::new(1.0, t.input_border));
|
||||
let tc = egui::pos2(r.right() - 28.0, r.center().y);
|
||||
let styled = !n.appearance.is_basic() || n.opacity < 1.0;
|
||||
let styled = has_styled_target(n);
|
||||
ui.painter().circle_stroke(tc, 5.0, Stroke::new(1.0, t.icon));
|
||||
if is_sel {
|
||||
ui.painter().circle_stroke(tc, 2.8, Stroke::new(1.0, t.icon));
|
||||
|
|
@ -292,6 +292,12 @@ fn row(
|
|||
|
||||
/// A real rendered thumbnail, cached by node identity (unchanged nodes keep their `Arc`
|
||||
/// allocation, so the address is a free change detector). Only rendered for visible rows.
|
||||
/// Whether an object's target circle is filled: its appearance is not basic or its transparency
|
||||
/// (opacity, blend mode, isolation, knockout or an opacity mask) is not the default.
|
||||
fn has_styled_target(n: &Node) -> bool {
|
||||
!n.appearance.is_basic() || !n.has_default_transparency()
|
||||
}
|
||||
|
||||
fn real_thumb(ui: &Ui, doc: &vectorcraft_doc::Document, n: &Node, r: egui::Rect) -> bool {
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
|
|
@ -351,3 +357,36 @@ fn thumb(ui: &Ui, n: &Node, r: egui::Rect) {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
use vectorcraft_engine::Session;
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Filled target circles drawn by one headless frame of the panel.
|
||||
fn filled_targets(app: &mut VectorcraftApp, ctx: &egui::Context) -> usize {
|
||||
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| show(app, ui));
|
||||
out.textures_delta.clear();
|
||||
out.shapes.iter().filter(|c| matches!(&c.shape, egui::Shape::Circle(cs) if cs.radius == 3.2 && cs.fill != Color32::TRANSPARENT)).count()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn target_circle_fills_for_non_default_transparency() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
let run = |app: &mut VectorcraftApp, id: &str, p: serde_json::Value| app.session.execute(id, &p).unwrap();
|
||||
run(&mut app, "file.new", json!({"width": 100, "height": 100}));
|
||||
let id = run(&mut app, "shape.rectangle", json!({"x": 0, "y": 0, "width": 50, "height": 50}))["id"].clone();
|
||||
run(&mut app, "select.set", json!({ "ids": [id] }));
|
||||
let ctx = egui::Context::default();
|
||||
assert_eq!(filled_targets(&mut app, &ctx), 0);
|
||||
run(&mut app, "transparency.set", json!({"blend": "multiply"}));
|
||||
assert_eq!(filled_targets(&mut app, &ctx), 1, "a Multiply object");
|
||||
run(&mut app, "transparency.set", json!({"blend": "normal", "knockout": true}));
|
||||
assert_eq!(filled_targets(&mut app, &ctx), 1, "a knockout group");
|
||||
run(&mut app, "transparency.set", json!({"knockout": false}));
|
||||
run(&mut app, "appearance.addStroke", json!({}));
|
||||
assert_eq!(filled_targets(&mut app, &ctx), 1, "two strokes");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -157,3 +157,30 @@ fn item_opacity_popup_sets_the_items_opacity() {
|
|||
assert_eq!(n.appearance.items[1].opacity(), 0.5);
|
||||
assert_eq!((n.appearance.items[0].opacity(), n.opacity), (1.0, 1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_row_names_the_selection_like_the_control_bar() {
|
||||
let mut app = app_with_rect();
|
||||
let a = first_selected(&app).unwrap().id.0;
|
||||
let ctx = egui::Context::default();
|
||||
assert!(frame_in(&ctx, &mut app, appearance::show).contains(&"Rectangle".to_string()));
|
||||
let b = run(&mut app, "shape.rectangle", json!({"x": 120, "y": 10, "width": 50, "height": 50}))["id"].clone();
|
||||
// Objects with one appearance list it under "Mixed Objects".
|
||||
run(&mut app, "select.set", json!({"ids": [a, b]}));
|
||||
let texts = frame_in(&ctx, &mut app, appearance::show);
|
||||
assert!(texts.contains(&"Mixed Objects".to_string()) && texts.contains(&"Stroke:".to_string()), "{texts:?}");
|
||||
// Objects that differ list nothing.
|
||||
run(&mut app, "select.set", json!({"ids": [b]}));
|
||||
run(&mut app, "transparency.set", json!({"opacity": 50}));
|
||||
run(&mut app, "select.set", json!({"ids": [a, b]}));
|
||||
let texts = frame_in(&ctx, &mut app, appearance::show);
|
||||
assert!(texts.contains(&"Mixed Appearances".to_string()) && !texts.contains(&"Stroke:".to_string()), "{texts:?}");
|
||||
// Type objects are "Type" in both places.
|
||||
let t = run(&mut app, "text.create", json!({"x": 10, "y": 150, "text": "Hi"}))["id"].clone();
|
||||
run(&mut app, "select.set", json!({ "ids": [t] }));
|
||||
assert_eq!(appearance::object_label(&app), "Type");
|
||||
assert!(frame_in(&ctx, &mut app, appearance::show).contains(&"Type".to_string()));
|
||||
let fonts = egui::Context::default();
|
||||
crate::theme::install_fonts(&fonts);
|
||||
assert!(frame_in(&fonts, &mut app, crate::chrome::control_bar).contains(&"Type".to_string()));
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue