mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 21:17:48 +00:00
Merge pull request #7102 from dotCooCoo/feat/layers-drag-autoscroll
Layers: layer and object rows held near the list's top or bottom edge scroll a long list
This commit is contained in:
@@ -521,6 +521,34 @@ pub(crate) fn rows_take_delete(ctx: &egui::Context) -> bool {
|
||||
has_row_keys(ctx, layers::ROW_KEYS) || has_row_keys(ctx, appearance::ROW_KEYS)
|
||||
}
|
||||
|
||||
/// How near a scrolled list's top or bottom edge a drag scrolls it ([`scroll_at_edges`]).
|
||||
const DRAG_SCROLL_EDGE: f32 = 28.0;
|
||||
|
||||
/// The speed of that scrolling in points per second: at the band's inner side, and added to it at
|
||||
/// the list's edge.
|
||||
const DRAG_SCROLL_SPEED: (f32, f32) = (60.0, 280.0);
|
||||
|
||||
/// While something is dragged near the top or bottom edge of a scrolled list (`ui`: its contents),
|
||||
/// scroll that way, faster nearer the edge, and draw the next frame so a pointer held still keeps
|
||||
/// it scrolling. The pointer must be over the list's width. Each frame scrolls by the time it
|
||||
/// took (a stalled frame counts a tenth of a second at most), so the speed is the same at every
|
||||
/// frame rate.
|
||||
pub(crate) fn scroll_at_edges(ui: &mut Ui) {
|
||||
let Some(pos) = ui.ctx().pointer_hover_pos() else { return };
|
||||
let view = ui.clip_rect();
|
||||
if !view.x_range().contains(pos.x) {
|
||||
return;
|
||||
}
|
||||
let (slow, more) = DRAG_SCROLL_SPEED;
|
||||
let speed = |depth: f32| if (0.0..DRAG_SCROLL_EDGE).contains(&depth) { slow + more * (1.0 - depth / DRAG_SCROLL_EDGE) } else { 0.0 };
|
||||
let per_second = speed(pos.y - view.top()) - speed(view.bottom() - pos.y);
|
||||
if per_second != 0.0 {
|
||||
let dt = ui.input(|i| i.stable_dt).min(0.1);
|
||||
ui.scroll_with_delta_animation(vec2(0.0, per_second * dt), egui::style::ScrollAnimation::none());
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
}
|
||||
|
||||
/// [`crate::i18n::label_or_name`] in the UI language.
|
||||
pub(crate) fn label_or_name(s: &str, built_in: bool) -> &str {
|
||||
crate::i18n::label_or_name(crate::i18n::current(), s, built_in)
|
||||
|
||||
@@ -19,7 +19,8 @@
|
||||
//! to Outline and the row's layer to Preview (`layer.outlineOthers`), or switches every layer to
|
||||
//! Preview when the others are in Outline (`layer.previewAll`).
|
||||
//! - Dragging rows moves them above, below or into a layer or group (`layer.move`, a drop line or
|
||||
//! a box shows where); Alt copies them. A dimmed copy of the row follows the pointer. Layers
|
||||
//! a box shows where); Alt copies them. A dimmed copy of the row follows the pointer, and rows
|
||||
//! held near the top or bottom edge of the list scroll it, faster nearer the edge. Layers
|
||||
//! dropped in the empty space under the last row go below the bottom layer. Dropped on the trash
|
||||
//! they are deleted, on New Layer duplicated.
|
||||
//! - Deleting rows from the panel (the trash, rows dropped on it, Delete Selection, Delete and
|
||||
@@ -304,6 +305,13 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
for r in list.get(range).unwrap_or_default() {
|
||||
row(ui, &view, r, &mut out);
|
||||
}
|
||||
// Rows held near the list's top or bottom edge scroll it, so a row out of view can
|
||||
// take the drop.
|
||||
if egui::DragAndDrop::payload::<LayersDrag>(ui.ctx()).is_some_and(|d| matches!(*d, LayersDrag::Rows(_)))
|
||||
&& ui.input(|i| i.pointer.primary_down())
|
||||
{
|
||||
super::scroll_at_edges(ui);
|
||||
}
|
||||
},
|
||||
)
|
||||
});
|
||||
|
||||
@@ -1232,6 +1232,202 @@ fn dragging_rows_moves_them_into_other_layers_and_alt_copies() {
|
||||
let _ = (layer, c);
|
||||
}
|
||||
|
||||
/// One headless frame of the panel on a short screen (300 × 260) with `events` (Alt held when
|
||||
/// `alt`): for every row name painted inside the list's view, its center, and the view.
|
||||
fn short_frame(app: &mut VectorcraftApp, ctx: &egui::Context, events: Vec<egui::Event>, alt: bool) -> (HashMap<String, egui::Pos2>, egui::Rect) {
|
||||
timed_short_frame(app, ctx, events, alt, 1.0 / 60.0)
|
||||
}
|
||||
|
||||
/// [`short_frame`] for a frame that comes `dt` seconds after the one before it.
|
||||
fn timed_short_frame(
|
||||
app: &mut VectorcraftApp,
|
||||
ctx: &egui::Context,
|
||||
events: Vec<egui::Event>,
|
||||
alt: bool,
|
||||
dt: f32,
|
||||
) -> (HashMap<String, egui::Pos2>, egui::Rect) {
|
||||
let mut events = events;
|
||||
events.insert(0, egui::Event::ModifiersChanged(egui::Modifiers { alt, ..Default::default() }));
|
||||
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, vec2(300.0, 260.0));
|
||||
let mut out = ctx.run_ui(egui::RawInput { events, screen_rect: Some(screen), predicted_dt: dt, ..Default::default() }, |ui| show(app, ui));
|
||||
out.textures_delta.clear();
|
||||
// The rows' eye column dividers are drawn clipped to the list's view.
|
||||
let border = Tokens::get(ctx).input_border;
|
||||
let view = out
|
||||
.shapes
|
||||
.iter()
|
||||
.find_map(|c| match &c.shape {
|
||||
egui::Shape::LineSegment { stroke, .. } if stroke.color == border && c.clip_rect.height() < screen.height() => Some(c.clip_rect),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(egui::Rect::NOTHING);
|
||||
let mut names = HashMap::new();
|
||||
for c in &out.shapes {
|
||||
if let egui::Shape::Text(t) = &c.shape
|
||||
&& t.galley.text().starts_with("Layer ")
|
||||
&& view.contains_rect(c.clip_rect)
|
||||
{
|
||||
let center = t.pos + t.galley.size() / 2.0;
|
||||
if c.clip_rect.contains(center) {
|
||||
names.insert(t.galley.text().to_string(), center);
|
||||
}
|
||||
}
|
||||
}
|
||||
(names, view)
|
||||
}
|
||||
|
||||
/// A document of 30 empty layers named `Layer 1` (the bottom one) to `Layer 30` (the top one):
|
||||
/// more rows than [`short_frame`]'s list shows.
|
||||
fn thirty_layers() -> VectorcraftApp {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.session.execute("file.new", &json!({"width": 200, "height": 200})).unwrap();
|
||||
for i in 2..=30 {
|
||||
app.session.execute("layer.new", &json!({"top": true, "name": format!("Layer {i}")})).unwrap();
|
||||
}
|
||||
app
|
||||
}
|
||||
|
||||
/// The layers' names, bottom first.
|
||||
fn layer_names(app: &VectorcraftApp) -> Vec<String> {
|
||||
app.session.active().unwrap().doc.layers.iter().map(|l| l.display_name()).collect()
|
||||
}
|
||||
|
||||
/// Hold the button still at `at` (a row is held) until `name`'s row is fully in view; → its center.
|
||||
fn hold_until(app: &mut VectorcraftApp, ctx: &egui::Context, at: egui::Pos2, name: &str, alt: bool) -> Option<egui::Pos2> {
|
||||
short_frame(app, ctx, vec![egui::Event::PointerMoved(at)], alt);
|
||||
for _ in 0..500 {
|
||||
let (names, view) = short_frame(app, ctx, vec![], alt);
|
||||
if let Some(&c) = names.get(name)
|
||||
&& c.y - ROW_MEDIUM / 2.0 >= view.top()
|
||||
&& c.y + ROW_MEDIUM / 2.0 <= view.bottom()
|
||||
{
|
||||
return Some(c);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rows_held_near_the_bottom_or_top_edge_scroll_the_list_to_a_row_out_of_view() {
|
||||
let mut app = thirty_layers();
|
||||
let ctx = egui::Context::default();
|
||||
let undo = |app: &VectorcraftApp| app.session.active().unwrap().history.undo.len();
|
||||
let (names, view) = short_frame(&mut app, &ctx, vec![], false);
|
||||
assert!(names.contains_key("Layer 30") && !names.contains_key("Layer 1"), "the list overflows: {names:?}");
|
||||
let before = (undo(&app), layer_names(&app));
|
||||
// Layer 30 held still just above the list's bottom edge: the list scrolls down to Layer 1.
|
||||
let grab = names["Layer 30"];
|
||||
short_frame(&mut app, &ctx, vec![egui::Event::PointerMoved(grab)], false);
|
||||
short_frame(&mut app, &ctx, vec![button(grab, true)], false);
|
||||
short_frame(&mut app, &ctx, vec![egui::Event::PointerMoved(grab + vec2(0.0, 6.0))], false);
|
||||
let edge = egui::pos2(grab.x, view.bottom() - 3.0);
|
||||
let target = hold_until(&mut app, &ctx, edge, "Layer 1", false).expect("Layer 1 scrolled into view while the row was held");
|
||||
assert_eq!((undo(&app), layer_names(&app)), before, "scrolling alone changes nothing");
|
||||
// Dropped on the upper edge of Layer 1's row: above it.
|
||||
let drop = target - vec2(0.0, ROW_MEDIUM * 0.4);
|
||||
short_frame(&mut app, &ctx, vec![egui::Event::PointerMoved(drop)], false);
|
||||
short_frame(&mut app, &ctx, vec![button(drop, false)], false);
|
||||
let names = layer_names(&app);
|
||||
assert_eq!(&names[..2], ["Layer 1", "Layer 30"], "{names:?}");
|
||||
assert_eq!(undo(&app), before.0 + 1);
|
||||
// Released, the list stays where it is with the pointer in the top band.
|
||||
let (rows, _) = short_frame(&mut app, &ctx, vec![egui::Event::PointerMoved(egui::pos2(drop.x, view.top() + 3.0))], false);
|
||||
for _ in 0..10 {
|
||||
assert_eq!(short_frame(&mut app, &ctx, vec![], false).0, rows, "no scrolling after the release");
|
||||
}
|
||||
// Layer 2, Alt-dragged to the top edge: the list scrolls back up, and dropped on the lower edge
|
||||
// of Layer 29's row a copy goes below it.
|
||||
let grab = rows["Layer 2"];
|
||||
short_frame(&mut app, &ctx, vec![egui::Event::PointerMoved(grab)], true);
|
||||
short_frame(&mut app, &ctx, vec![button(grab, true)], true);
|
||||
short_frame(&mut app, &ctx, vec![egui::Event::PointerMoved(grab - vec2(0.0, 6.0))], true);
|
||||
let (_, view) = short_frame(&mut app, &ctx, vec![], true);
|
||||
let target = hold_until(&mut app, &ctx, egui::pos2(grab.x, view.top() + 3.0), "Layer 29", true).expect("Layer 29 scrolled into view");
|
||||
let drop = target + vec2(0.0, ROW_MEDIUM * 0.4);
|
||||
short_frame(&mut app, &ctx, vec![egui::Event::PointerMoved(drop)], true);
|
||||
short_frame(&mut app, &ctx, vec![button(drop, false)], true);
|
||||
let names = layer_names(&app);
|
||||
assert_eq!(names.len(), 31);
|
||||
assert_eq!((names[28].as_str(), names[30].as_str()), ("Layer 28", "Layer 29"), "{names:?}");
|
||||
assert!(names[29].starts_with("Layer 2"), "the copy, below Layer 29: {names:?}");
|
||||
assert_eq!(names[2], "Layer 2", "the original stays");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_held_row_scrolls_the_list_as_far_in_the_same_time_at_any_frame_rate() {
|
||||
// Layer 30 held just above the list's bottom edge for half a second, drawn in `frames` frames,
|
||||
// then moved beside the list, where the scrolling stops: → where the rows in view came to rest.
|
||||
let held = |frames: u16| {
|
||||
let mut app = thirty_layers();
|
||||
let ctx = egui::Context::default();
|
||||
let (names, view) = short_frame(&mut app, &ctx, vec![], false);
|
||||
let grab = names["Layer 30"];
|
||||
short_frame(&mut app, &ctx, vec![egui::Event::PointerMoved(grab)], false);
|
||||
short_frame(&mut app, &ctx, vec![button(grab, true)], false);
|
||||
short_frame(&mut app, &ctx, vec![egui::Event::PointerMoved(grab + vec2(0.0, 6.0))], false);
|
||||
let dt = 0.5 / f32::from(frames);
|
||||
timed_short_frame(&mut app, &ctx, vec![egui::Event::PointerMoved(egui::pos2(grab.x, view.bottom() - 3.0))], false, dt);
|
||||
for _ in 1..frames {
|
||||
timed_short_frame(&mut app, &ctx, vec![], false, dt);
|
||||
}
|
||||
short_frame(&mut app, &ctx, vec![egui::Event::PointerMoved(egui::pos2(view.right() + 2.0, view.bottom() - 3.0))], false);
|
||||
let mut rows = HashMap::new();
|
||||
for _ in 0..120 {
|
||||
rows = short_frame(&mut app, &ctx, vec![], false).0;
|
||||
}
|
||||
// (Layer 30's dimmed copy follows the pointer.)
|
||||
rows.remove("Layer 30");
|
||||
rows
|
||||
};
|
||||
let (at_30, at_120) = (held(15), held(60));
|
||||
assert!(!at_30.contains_key("Layer 29") && !at_30.is_empty(), "the list scrolled: {at_30:?}");
|
||||
assert_eq!(at_30.len(), at_120.len(), "{at_30:?} {at_120:?}");
|
||||
for (name, a) in &at_30 {
|
||||
let b = at_120.get(name).unwrap_or_else(|| panic!("{name} is in view at 30 frames a second only: {at_120:?}"));
|
||||
assert!((a.y - b.y).abs() < 2.0, "{name}: {a:?} at 30 frames a second, {b:?} at 120");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_list_scrolls_only_for_rows_held_over_it() {
|
||||
let mut app = thirty_layers();
|
||||
let ctx = egui::Context::default();
|
||||
let (rows, view) = short_frame(&mut app, &ctx, vec![], false);
|
||||
// The pointer in the bottom band with no button down: nothing scrolls.
|
||||
let edge = egui::pos2(150.0, view.bottom() - 3.0);
|
||||
short_frame(&mut app, &ctx, vec![egui::Event::PointerMoved(edge)], false);
|
||||
for _ in 0..20 {
|
||||
assert_eq!(short_frame(&mut app, &ctx, vec![], false).0, rows, "hovering scrolls nothing");
|
||||
}
|
||||
// A row held in the bottom band beside the list (right of its view): nothing scrolls, and
|
||||
// the release there moves nothing.
|
||||
let before = layer_names(&app);
|
||||
let grab = rows["Layer 30"];
|
||||
short_frame(&mut app, &ctx, vec![egui::Event::PointerMoved(grab)], false);
|
||||
short_frame(&mut app, &ctx, vec![button(grab, true)], false);
|
||||
short_frame(&mut app, &ctx, vec![egui::Event::PointerMoved(grab + vec2(0.0, 6.0))], false);
|
||||
let beside = egui::pos2(view.right() + 2.0, view.bottom() - 3.0);
|
||||
short_frame(&mut app, &ctx, vec![egui::Event::PointerMoved(beside)], false);
|
||||
// (Layer 30's dimmed copy follows the pointer, so another row is compared.)
|
||||
assert!(rows.contains_key("Layer 27"));
|
||||
for _ in 0..20 {
|
||||
assert_eq!(short_frame(&mut app, &ctx, vec![], false).0.get("Layer 27"), rows.get("Layer 27"), "held beside the list");
|
||||
}
|
||||
short_frame(&mut app, &ctx, vec![button(beside, false)], false);
|
||||
short_frame(&mut app, &ctx, vec![], false);
|
||||
assert_eq!(layer_names(&app), before);
|
||||
// The eye column dragged down into the band: a column drag, which scrolls nothing.
|
||||
let eye = egui::pos2(12.0, grab.y);
|
||||
short_frame(&mut app, &ctx, vec![egui::Event::PointerMoved(eye)], false);
|
||||
short_frame(&mut app, &ctx, vec![button(eye, true)], false);
|
||||
short_frame(&mut app, &ctx, vec![egui::Event::PointerMoved(eye + vec2(0.0, 6.0))], false);
|
||||
short_frame(&mut app, &ctx, vec![egui::Event::PointerMoved(egui::pos2(12.0, view.bottom() - 3.0))], false);
|
||||
for _ in 0..20 {
|
||||
assert_eq!(short_frame(&mut app, &ctx, vec![], false).0, rows, "a column drag");
|
||||
}
|
||||
short_frame(&mut app, &ctx, vec![button(egui::pos2(12.0, view.bottom() - 3.0), false)], false);
|
||||
}
|
||||
|
||||
/// The centres of the bottom bar's buttons, right to left from under the right end of its top
|
||||
/// divider: Delete, New Layer, New Sublayer, Make/Release Clipping Mask, Locate Object.
|
||||
fn bar_buttons(app: &mut VectorcraftApp, ctx: &egui::Context) -> Vec<egui::Pos2> {
|
||||
|
||||
@@ -51,8 +51,6 @@ const LIST_VIEW: &str = "library-list-view";
|
||||
const BAR: f32 = 42.0;
|
||||
/// The least height the list takes.
|
||||
const MIN_LIST: f32 = 140.0;
|
||||
/// How near the list's top or bottom edge a drag scrolls it.
|
||||
const EDGE: f32 = 28.0;
|
||||
|
||||
/// The current library: its id and a copy of it (the panel menu acts on it after reading).
|
||||
fn current(app: &VectorcraftApp) -> Option<(String, Library)> {
|
||||
@@ -329,7 +327,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
}
|
||||
});
|
||||
if items.grab.is_some() {
|
||||
scroll_at_edges(ui);
|
||||
super::scroll_at_edges(ui);
|
||||
}
|
||||
});
|
||||
out.inner_rect
|
||||
@@ -713,22 +711,6 @@ fn no_group_strip(ui: &mut Ui) {
|
||||
ui.add_space(6.0);
|
||||
}
|
||||
|
||||
/// While something is dragged near the top or bottom edge of the scrolled list (`ui`: its
|
||||
/// contents), scroll that way, faster nearer the edge.
|
||||
fn scroll_at_edges(ui: &mut Ui) {
|
||||
let Some(pos) = ui.ctx().pointer_hover_pos() else { return };
|
||||
let view = ui.clip_rect();
|
||||
if !view.x_range().contains(pos.x) {
|
||||
return;
|
||||
}
|
||||
let speed = |depth: f32| if (0.0..EDGE).contains(&depth) { 2.0 + 10.0 * (1.0 - depth / EDGE) } else { 0.0 };
|
||||
let delta = speed(pos.y - view.top()) - speed(view.bottom() - pos.y);
|
||||
if delta != 0.0 {
|
||||
ui.scroll_with_delta(vec2(0.0, delta));
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
}
|
||||
|
||||
/// The drag in progress held over a section of the list (`sections`, seen through `clip`), away
|
||||
/// from the items that take one: an item goes into that section's group (the ungrouped section:
|
||||
/// out of its group), and something new is added there, the section outlined; a group goes
|
||||
|
||||
@@ -510,7 +510,7 @@ about 1–1.5 agent-hours each, so the other rows (estimated on the older scale)
|
||||
| Type core | 9 | 80% | Middle Eastern features beyond bidi layout, Paragraph Direction, digit types and Kashidas (the Middle Eastern composers, editable bidirectional SVG text, a split caret where directions meet); paint-only harakat keep joining with conditional GDEF-mark colour ownership, but fonts without GDEF mark classes and some AAT/`morx` ligatures can still leave marks painted as their letters; Every-line Composer fidelity for justified and ragged text (available per object; mixed type sizes in a paragraph still break single-line in non-rectangular area type and with text wrap), Optical Margin Alignment, paragraph spacing limit for vertically justified area type, hidden characters, paragraph attributes in the Type tool's copy and paste (the characters keep their styles, the paragraphs take the destination's), mixed-value display in the Paragraph panel for a selection across differing paragraphs, partial selection of type-area anchors by marquee, optional font-size preservation during group scaling (scaled character controls fixed in #1034). Hyphenation and Justification options, Fractional Widths, System Layout, styled Type-range canvas Paste (#5636), and pattern hyphenation for eleven Indian languages with grapheme/cluster protection (#4947) are done; Sanskrit automatic patterns remain pending an allowlisted source | 14–19 |
|
||||
| Symbols, blends, envelopes, Repeat, perspective | 5 | 90% | further built-in symbol libraries (Basic Shapes, UI Frames with default 9-slice, Map Markers, Inventory Icons and Skill Icons are done), dynamic symbols' `.ai` round trip, envelope Anti-Alias/Preserve Shape output and warp point editing, brush interpolation in blends (M8 fidelity pass done); Symbol Options › Registration and Export Type and on-canvas dynamic part paint (gradient/pattern, opacity, blend) are done; symbol editing shows draggable 9-slice guides (#5280), while guides outside the artwork remain unsupported | 4–7 |
|
||||
| Image Trace, graphs, image tools | 3 | 72% | graph Column designs (Graph Design, Marker designs and series colours set with Group Selection are done), Create Object Mosaic, per-feature stroke/fill choice and varying widths in Image Trace's Create Strokes (Vector Halftone and the Image Trace View modes, Palette and Create Strokes are done) | 6–10 |
|
||||
| Layers, artboards, document setup | 5 | 82% | Layers panel: a confirmation before deleting layers that hold art, Collect for Export, row context menus, the Layer Options dialog for several rows with mixed values, Paste Remembers Layers for sublayers, drag-scrolling a long panel while dragging rows; artboard preset polish (artboard escape routing and document setup modal handoff done; Move Artwork for artboard X/Y edits, Rearrange All Artboards, Document Setup, New Document and the rest of the Layers panel are done) | 4–7 |
|
||||
| Layers, artboards, document setup | 5 | 82% | Layers panel: a confirmation before deleting layers that hold art, Collect for Export, row context menus, the Layer Options dialog for several rows with mixed values, Paste Remembers Layers for sublayers; artboard preset polish (artboard escape routing and document setup modal handoff done; Move Artwork for artboard X/Y edits, Rearrange All Artboards, Document Setup, New Document and the rest of the Layers panel are done) | 4–7 |
|
||||
| View & navigation | 3 | 70% | New View/Edit Views, multiple windows/arrange, Snap to Pixel/Glyph (print tiling is done) | 9–14 |
|
||||
| Guides, grids, smart guides, snapping, rulers | 3 | 85% | global/video rulers, Spacing Guides' distance readouts and spacing for resized or drawn art, Alt swapping a ruler guide's orientation, ruler guides selected together with art (marquee, Shift-click), ruler guides as Layers rows with their own lock and hide (#451: Lock Guides locks them all; Make Guides paths are Layers rows already) (dragging existing guides is done, #414; new guides snapping to side midpoints and artboards, and artboard guides, #451; the drawing tools snapping, with Construction Guides, #506; Spacing Guides for moves, #394; moves snapping to the grid, #740; ruler guides pulling drawn points with Smart Guides on, and "center" and "anchor" labels while hovering with the Selection tool, #812; distance and angle labels while drawing, #2983) | 2–4; status-bar X/Y uses the active ruler origin and General units |
|
||||
| File formats | 6 | 85% | PDF import: glyphs inside a marked-content span with an empty `ActualText` come in as outlines in their fill and stroke, adding no text (#6971; scores and estimates unchanged); Explicit editing-data oversized single-artboard import fixed (#6865); scores and estimates unchanged. Affinity export, and the rest of Affinity import (adjustments, blur and bevel effects, brushes, live symbol instances, text threading), DWG (no open spec: DXF instead), PSD placement as layers (Photoshop Import Options; the merged image is done), EPS checked against more real files users send (Inkscape files open as vectors; other generators' constructs are tested with programs written from the PostScript Language Reference only; CID-keyed CJK type and `CCITTFaxDecode` images not read), legacy `.ai` files saved without the prolog that defines their operators (their own operator set isn't read yet), the rest of Illustrator's editing data (appearances and live effects, symbols, pattern fills, brushes, placed files: #637), the remaining fidelity polish; EPS, DXF, EMF/WMF, TIFF, BMP, Targa, PSD export, SVGZ, PDF security, PDF/X, PDF layers, presets and Affinity import are done | 6–10 (existing scope; Affinity export unestimated) |
|
||||
@@ -842,6 +842,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- Screen readers get the value of the color and channel sliders that panels and dialogs draw (the Color panel, the Color Picker, Swatch Options, the Navigator's Zoom, the Character panel's variable font axes, the Perspective Grid and flattener options, and the sliders of the effect, tool option and transform dialogs): the number and text of the field beside each one, such as 128 for G or 50% for an opacity, with the slider's range, which extends to include a number past the slider's end. The harmony wheel's Brightness slider, which has no field, reports its brightness in percent.
|
||||
- A layer whose Layer Options › Print is off shows its name italic in the Layers panel, and while its row is dragged, as the reference application shows non-printing layers (#5739); a printing layer stays upright and ticking Print again restores the upright name (`panels::layers` test `a_layer_that_does_not_print_has_its_name_italic`).
|
||||
- In the Layers panel only the primary mouse button drags. A drag with the right or middle button on a row, a guide row, the selection square, the eye or lock column or a target circle moves, copies, hides, locks and transfers nothing and records no undo step, and a right-click still opens the row's menu (`panels::layers` test `only_the_primary_button_drags_rows_guides_art_columns_and_appearances`).
|
||||
- Rows dragged in the Layers panel and held within 28 points of the list's top or bottom edge scroll a long list that way, faster nearer the edge (60 points a second at the band's inner side, 340 at the edge, at every frame rate), and keep scrolling while the pointer stays still, so a row out of view can take the drop (a move, or a copy with Alt); the Libraries panel's list scrolls by the same rule. A pointer beside the list, a drag of the eye or lock column and a release stop the scrolling, and scrolling changes nothing in the document. The band and speeds are VectorCraft's own (`panels::layers` tests `rows_held_near_the_bottom_or_top_edge_scroll_the_list_to_a_row_out_of_view`, `a_held_row_scrolls_the_list_as_far_in_the_same_time_at_any_frame_rate` and `the_list_scrolls_only_for_rows_held_over_it`).
|
||||
- The other panels' item drags start only with the primary mouse button too: swatches and color groups, brushes, symbols, graphic styles, library items and groups, character and paragraph styles, Appearance rows and its thumbnail, the Fill and Stroke squares, Recent Colors and the Gradient panel's thumbnail. A right or middle drag on one carries nothing, moves, copies, deletes and applies nothing and records no undo step; right-click menus and the Graphic Styles panel's held right-button preview stay as they were (`panels::swatches` test `only_a_primary_drag_moves_swatches_and_groups`, `panels::text_styles` test `only_a_primary_drag_carries_a_style_row`, `panels::tests_symbol_libraries` test `only_a_primary_drag_carries_a_library_symbol_out`, `panels::tests_effectedit` test `only_a_primary_drag_carries_the_thumbnail_or_a_row`).
|
||||
- Panels undock (#495): a dock tab, a panel icon or a popped-out panel's title dragged out of the dock floats that panel (in an OS window of its own on the desktop, #4135), and the strip right of the dock's tabs floats the whole tabbed group. A floating group moves by its title bar, a tab dragged out of it floats on its own, and dropped on another group's title bar or tabs it stacks with that group; dropped on a dock group's tabs, which light up under the pointer, its panels join that group, and on the group's upper or lower half they make a new group above or below it (`window.panel.dock {anchor, zone}`); dropped on the icon column they become icons there, the tabbed group's panels too (`window.panel.dock {icon: true}`); dropped anywhere else on the dock, which lights up, they go back where they live (the tabbed group or the icon column), as its × and the panel menus' Float Panel / Dock Panel do (`window.panel.float`, `window.panel.dock`). The Tools panel floats by its title bar and docks again on the window's left edge. Floating groups stack into sets: dropped on the upper or lower half of another group (a line shows where), a group joins it right above or below it, and the set's groups stack top to bottom, each showing one panel, moving together by the top group's title bar, whose × docks them all; a group dragged by its tab strip leaves the set (`window.panel.float {below, above}`). A double-click on a tab collapses a floating group to its tabs (`collapsed`), and a dock group too, a group below it moving up into the room; a second double-click, or a click on another of its tabs, expands it (`window.panel.collapse`, kept with the preferences and in saved workspaces). Sets dropped on the left edge of the icon column or of another dock column dock as dock columns (a multi-column dock, `window.panel.dock {column}`), whose groups stack, tab and tear out as a floating set's do, and whose top strip floats the column again. Floating panels, their sets and the Tools panel's position are kept with the preferences and in user workspaces, and clamped into a smaller window. Not done: panels in separate OS windows, reordering tabs by dragging, collapsing a set or dock column to icons, dock columns right of the default dock, resizing floating panels and the groups of a set.
|
||||
- A floating panel's OS window hides while a tool group's flyout or the All Tools drawer is open in the main window, as it does under a modal dialog or a menu, and shows again when the flyout or the drawer closes (#5813; `a_tool_flyout_hides_the_palette_windows_until_it_closes`).
|
||||
|
||||
Reference in New Issue
Block a user