mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-01 06:06:49 -06:00
Merge PR #1389: Properties: the arrow beside Opacity opens a slider, as in the reference app
Conflicts: crates/ui-egui/src/panels/properties/object.rs: kept both new functions (open_color_picker from #1499, opacity_slider from #1389)
This commit is contained in:
@@ -458,6 +458,7 @@ Newest first.
|
||||
- **2026-10-21** · Effects dialog: the Angle field of the shadows, Bevel and Emboss, Satin and Gradient Feather has InDesign's dial beside it, its radius pointing where the light comes from; a click or drag on it sets the angle. Found in the Learn Skills Daily course, chapter 14. No readiness percentage change.
|
||||
- **2026-10-21** · A double-click on a Fill or Stroke chip of Properties › Appearance opens the Color Picker with its colour, as in InDesign (a click still opens the swatches); for selected text, its own chips open it for the text. Found in the Learn Skills Daily course, chapter 23. No readiness percentage change.
|
||||
- **2026-10-20** · Properties › Appearance: with several text frames selected, the Fill and Stroke popups offer Apply To Frame or Text, as with one frame, so their text can be coloured at once, as in InDesign. No readiness percentage change.
|
||||
- **2026-10-20** · Properties › Appearance: the arrow beside Opacity opens a slider under the field, as in InDesign, instead of the Effects panel; a drag shows the opacity live and is one undo step (the Opacity link still opens Effects). No readiness percentage change.
|
||||
- **2026-10-21** · Properties › Character and Paragraph: ••• (More Options) expands the section in place, as InDesign's: Character adds vertical and horizontal scale, baseline shift, skew, language and OpenType, and All Caps, Small Caps, Superscript, Subscript, Underline and Strikethrough; Paragraph adds the left, right, first line and last line indents, space before and after, the drop cap's lines and characters, Align to Baseline Grid and Hyphenate. It used to open the Character or Paragraph panel; the language field moved under ••• as in InDesign. Found in the Learn Skills Daily course, chapter 16. No readiness percentage change.
|
||||
- **2026-10-21** · The text Control panel has InDesign's two modes: A (Character Formatting Controls: font and style, size and leading, All Caps/Superscript/Underline and Small Caps/Subscript/Strikethrough, kerning and tracking, vertical and horizontal scale, baseline shift and skew, character style and language) and ¶ (Paragraph Formatting Controls: the nine alignments, left/first line and right/last line indents, space before and after, the drop cap's lines and characters, paragraph style, Hyphenate and Align to Baseline Grid). It used to show a fixed mix of both. Found in the Learn Skills Daily course, chapter 16. No readiness percentage change.
|
||||
- **2026-10-20** · With the Type tool, the Control panel's font size, leading and tracking fields have a spinner and a list of common values, as in InDesign (auto leading in parentheses). No readiness percentage change.
|
||||
|
||||
@@ -365,9 +365,8 @@ pub(super) fn appearance_section(app: &mut DesignApp, ui: &mut Ui, i: &SelInfo)
|
||||
let mresp = ui.interact(more, ui.scope_id().with("opmore"), Sense::click());
|
||||
ui.painter().rect(more, 0.0, if mresp.hovered() { t.hover } else { t.input }, Stroke::new(1.0, t.field_border), StrokeKind::Inside);
|
||||
icons::paint(ui.painter(), more.shrink(4.0), "chevron-right", t.icon);
|
||||
if mresp.clicked() {
|
||||
show_panel(app, "effects");
|
||||
}
|
||||
mresp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, crate::i18n::tr(&app.ui.language, "Opacity")));
|
||||
opacity_slider(app, &mresp, i.opacity);
|
||||
ui.add_space(6.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 10.0;
|
||||
@@ -388,6 +387,46 @@ pub(super) fn open_color_picker(app: &mut DesignApp, doc: &designcraft_doc::Docu
|
||||
app.ui.dialog = Some(crate::dialogs::Dialog::new("colorPicker", fields));
|
||||
}
|
||||
|
||||
/// The Opacity slider that the arrow beside the field opens, as in InDesign: a drag shows the
|
||||
/// new opacity live and is one undo step when let go.
|
||||
fn opacity_slider(app: &mut DesignApp, resp: &egui::Response, opacity: f64) {
|
||||
let (mut slid, mut held, mut shown) = (None, false, false);
|
||||
egui::Popup::menu(resp).close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside).show(|ui| {
|
||||
shown = true;
|
||||
// A thin rail and a round knob, as InDesign draws it.
|
||||
ui.spacing_mut().slider_width = 130.0;
|
||||
ui.spacing_mut().slider_rail_height = 2.0;
|
||||
let mut v = (opacity * 100.0).round();
|
||||
let slider = ui.add(
|
||||
egui::Slider::new(&mut v, 0.0..=100.0).integer().show_value(false).handle_shape(egui::style::HandleShape::Circle).trailing_fill(false),
|
||||
);
|
||||
// The slider has no visible caption (the field beside the arrow is the label), so it's
|
||||
// named for assistive technology only.
|
||||
let label = crate::i18n::tr(&app.ui.language, "Opacity");
|
||||
slider.widget_info(|| egui::WidgetInfo::slider(ui.is_enabled(), v, label));
|
||||
// The release frame can move the value too; it stays part of the drag's preview, which
|
||||
// the next frame commits as the one undo step.
|
||||
let pressed = slider.is_pointer_button_down_on() || slider.dragged() || slider.drag_stopped();
|
||||
if slider.changed() {
|
||||
slid = Some((v, pressed));
|
||||
}
|
||||
held = pressed;
|
||||
});
|
||||
match slid {
|
||||
Some((v, true)) => app.live_preview("Opacity", "object.opacity", json!({"opacity": v / 100.0})),
|
||||
Some((v, false)) => {
|
||||
// The keyboard (arrow keys on the focused slider): applied at once.
|
||||
app.end_live_preview();
|
||||
let _ = app.run("object.opacity", json!({"opacity": v / 100.0}));
|
||||
}
|
||||
None if held => app.hold_live_preview(),
|
||||
// Shown but let go without a change (a click outside, Escape): the drag's preview is
|
||||
// committed now, before whatever that click does, so that keeps its own undo step.
|
||||
None if shown => app.end_live_preview(),
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// The Fill (or Stroke) popup of Properties › Appearance, as InDesign's: Apply To (a text frame's
|
||||
/// frame or its text), the tint, then the swatches. `cur` is the frame's swatch. With `text_only`
|
||||
/// (text selected) it colours the selected text.
|
||||
|
||||
@@ -770,6 +770,60 @@ fn several_shapes_show_pathfinder_and_intersect_combines_them() {
|
||||
assert!((r.x0 - 200.0).abs() < 0.5 && (r.y1 - 200.0).abs() < 0.5 && (r.x1 - 300.0).abs() < 0.5, "{r:?}");
|
||||
}
|
||||
|
||||
/// The arrow beside Opacity opens a slider, as in InDesign: a drag shows the opacity live and
|
||||
/// is one undo step; the Opacity link still opens the Effects panel.
|
||||
#[test]
|
||||
fn the_opacity_arrow_opens_a_slider_and_a_drag_is_one_undo_step() {
|
||||
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({})).unwrap();
|
||||
let id = app.run("frame.create", json!({"rect": [72, 72, 300, 200], "shape": "rectangle"})).unwrap()["id"].as_u64().unwrap();
|
||||
app.run("selection.set", json!({"ids": [id]})).unwrap();
|
||||
let opacity = |app: &DesignApp| app.session.active().unwrap().doc.item(designcraft_doc::ids::ItemId(id)).unwrap().opacity;
|
||||
let steps = undo_steps(&app);
|
||||
let mut h = Harness::builder().with_size(vec2(320.0, 1400.0)).build_ui_state(
|
||||
|ui, app: &mut DesignApp| {
|
||||
let id = egui::Id::unique("test_fonts");
|
||||
if ui.data(|d| d.get_temp::<bool>(id)).is_none() {
|
||||
crate::theme::install_fonts(ui.ctx(), "");
|
||||
ui.data_mut(|d| d.insert_temp(id, true));
|
||||
return;
|
||||
}
|
||||
app.settle_live_preview();
|
||||
show(app, ui);
|
||||
},
|
||||
app,
|
||||
);
|
||||
h.run_steps(3);
|
||||
assert!(h.query_by_role(egui::accesskit::Role::Slider).is_none());
|
||||
h.get_by(|n| n.role() == egui::accesskit::Role::Button && n.label().as_deref() == Some("Opacity")).click();
|
||||
h.run_steps(3);
|
||||
assert!(
|
||||
h.query_by(|n| n.role() == egui::accesskit::Role::Slider && n.label().as_deref() == Some("Opacity")).is_some(),
|
||||
"assistive technology names the slider"
|
||||
);
|
||||
let track = h.get_by_role(egui::accesskit::Role::Slider).rect();
|
||||
let y = track.center().y;
|
||||
h.hover_at(egui::pos2(track.right() - 1.0, y));
|
||||
h.run_steps(1);
|
||||
h.drag_at(egui::pos2(track.right() - 1.0, y));
|
||||
h.run_steps(1);
|
||||
for k in 1..=5 {
|
||||
h.hover_at(egui::pos2(track.right() - track.width() * 0.05 * k as f32, y));
|
||||
h.run_steps(1);
|
||||
assert_eq!(undo_steps(h.state()), steps, "no undo step while dragging");
|
||||
}
|
||||
let previewed = opacity(h.state());
|
||||
assert!(previewed < 0.9 && previewed > 0.5, "the drag shows the opacity live: {previewed}");
|
||||
// Let go further along than the last move: the release frame changes the value too.
|
||||
h.drop_at(egui::pos2(track.right() - track.width() * 0.3, y));
|
||||
h.run_steps(3);
|
||||
assert_eq!(undo_steps(h.state()), steps + 1, "the whole drag is one undo step");
|
||||
assert!(opacity(h.state()) < previewed, "the release position is applied: {previewed} → {}", opacity(h.state()));
|
||||
assert_ne!(h.state().ui.open_panel.as_deref(), Some("effects"), "the arrow doesn't open Effects");
|
||||
h.state_mut().run("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(opacity(h.state()), 1.0);
|
||||
}
|
||||
|
||||
const CYAN: &str = "C=100 M=0 Y=0 K=0";
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user