mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 17:56:50 -06:00
Libraries panel: the list scrolls; items and groups reorder by dragging, items drag into groups
The list fills the panel down to its bottom bar and scrolls, also while a drag nears its top or
bottom edge (in the dock and in a flyout, as the Layers panel does).
Dragging in the list:
- an item (colour, text style or graphic) dropped on another of its kind lands just before or
after it, by the pointer's half, an accent line showing where, and joins that item's group;
- dropped elsewhere on a group (its header too, even collapsed) it goes into that group, and on
the ungrouped items out of its group, the section outlined; with every item grouped, a
"Drop here to take it out of its group" strip shows while one is dragged;
- a group dragged by its header goes before or after another group.
Colours and graphics still drag onto art and the canvas as before.
Engine: library.moveItem takes `before` / `after` (an item of the same kind) to place the item
in its kind's order, which the panel shows; the new library.moveGroup {group, to} moves a group.
Text styles drag as PanelDrag::LibraryItem, groups as PanelDrag::LibraryGroup, and a library
colour's Paint drag names its library item, so the panel tells it from a swatch dragged in.
Tests: reordering and moving in the engine, the commands in the library fuzz test, and panel
tests that drop styles and group headers and check the list scrolls and scrolls while dragging.
Strings translated in the 23 catalogs; docs/mcp.md and the parity row updated.
This commit is contained in:
@@ -5,7 +5,8 @@
|
||||
//! one copies the selection as Copy does, and placing it pastes a copy as Paste does (its images,
|
||||
//! symbols, patterns and swatches come along; a swatch name the document gives another colour is
|
||||
//! merged into the document's). Items can be put in user-named groups, and a library can be
|
||||
//! exported to a `.vclibrary` file and imported from one (#926).
|
||||
//! exported to a `.vclibrary` file and imported from one (#926). Items and groups are kept in the
|
||||
//! order the Libraries panel shows them, which dragging them there changes.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
@@ -281,6 +282,25 @@ impl Library {
|
||||
}
|
||||
}
|
||||
|
||||
/// Move item `r` next to `to`, an item of its kind: just before it, or just after it when
|
||||
/// `after`, in their kind's list (the order the panel shows them in).
|
||||
fn place_next_to(&mut self, r: &ItemRef, to: &ItemRef, after: bool) {
|
||||
fn shift<T>(list: &mut Vec<T>, key: impl Fn(&T) -> &str, from: &str, to: &str, after: bool) {
|
||||
let Some(i) = list.iter().position(|x| key(x) == from) else { return };
|
||||
let item = list.remove(i);
|
||||
let j = list.iter().position(|x| key(x) == to).map_or(list.len(), |j| j + usize::from(after));
|
||||
list.insert(j.min(list.len()), item);
|
||||
}
|
||||
let (from, to_key) = (r.item.as_str(), to.item.as_str());
|
||||
match r.kind {
|
||||
ItemKind::Color => shift(&mut self.colors, |c| c.name.as_str(), from, to_key, after),
|
||||
ItemKind::CharStyle => shift(&mut self.char_styles, |c| c.name.as_str(), from, to_key, after),
|
||||
ItemKind::ParaStyle => shift(&mut self.para_styles, |c| c.name.as_str(), from, to_key, after),
|
||||
ItemKind::Graphic => shift(&mut self.graphics, |g| g.id.as_str(), from, to_key, after),
|
||||
ItemKind::Unknown => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Make a library read from a file (hand-edited, imported) safe and consistent: at most
|
||||
/// [`MAX_ITEMS`] items of each kind and [`MAX_GROUPS`] groups, names clipped to [`MAX_NAME`]
|
||||
/// characters and unique within their kind (a repeated one gets a number), sizes finite, a
|
||||
@@ -552,10 +572,19 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Move Library Item to Group",
|
||||
[],
|
||||
None,
|
||||
"{library?, kind, item: name (graphic: id or name), group?: name (null or none: out of its group)} put an item in a group (out of the one it was in) → {library, kind, item, group: name or null}",
|
||||
"{library?, kind, item: name (graphic: id or name), group?: name (null or none: out of its group), before?: item | after?: item (an item of the same kind, named as `item` is)} put an item in a group (out of the one it was in); with `before` or `after`, also move it just before or after that item in the order its kind is shown in, as dragging it in the Libraries panel does → {library, kind, item, group: name or null}",
|
||||
always,
|
||||
move_item
|
||||
),
|
||||
cmd!(
|
||||
"library.moveGroup",
|
||||
"Move Library Group",
|
||||
[],
|
||||
None,
|
||||
"{library?, group: name, to: index} move a group to position `to` of the library's groups (0 = first; past the end: last), as dragging its header in the Libraries panel does → {library, group, to: where it went}",
|
||||
always,
|
||||
move_group
|
||||
),
|
||||
cmd!(
|
||||
query "library.export",
|
||||
"Export Library",
|
||||
@@ -1023,13 +1052,30 @@ fn move_item(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "library.moveItem";
|
||||
let (id, kind, item) = item_param(s, p, C)?;
|
||||
let group = str_param(p, "group");
|
||||
let next_to = match (str_param(p, "before"), str_param(p, "after")) {
|
||||
(Some(_), Some(_)) => return Err(bad(C, "give `before` or `after`, not both")),
|
||||
(Some(b), None) => Some((b, false)),
|
||||
(None, Some(a)) => Some((a, true)),
|
||||
(None, None) => None,
|
||||
};
|
||||
let (key, to) = s.libraries.change(&id, |lib| {
|
||||
let r = lib.item_ref(kind.item_kind(), item).ok_or_else(|| bad(C, format!("the library has no item `{item}`")))?;
|
||||
let missing = |item: &str| bad(C, format!("the library has no item `{item}`"));
|
||||
let r = lib.item_ref(kind.item_kind(), item).ok_or_else(|| missing(item))?;
|
||||
let at = group.map(|g| group_at(lib, g, C)).transpose()?;
|
||||
let next_to = next_to
|
||||
.map(|(other, after)| Ok::<_, EngineError>((lib.item_ref(kind.item_kind(), other).ok_or_else(|| missing(other))?, after)))
|
||||
.transpose()?;
|
||||
if let Some((other, after)) = &next_to
|
||||
&& *other != r
|
||||
{
|
||||
lib.place_next_to(&r, other, *after);
|
||||
}
|
||||
lib.ungroup(&r);
|
||||
let key = r.item.clone();
|
||||
let to = at.and_then(|i| lib.groups.get_mut(i)).map(|g| {
|
||||
g.items.push(r);
|
||||
// Next to the other item when that is in the group too, else last.
|
||||
let near = next_to.as_ref().and_then(|(other, after)| g.items.iter().position(|x| x == other).map(|j| j + usize::from(*after)));
|
||||
g.items.insert(near.unwrap_or(g.items.len()).min(g.items.len()), r);
|
||||
g.name.clone()
|
||||
});
|
||||
Ok((key, to))
|
||||
@@ -1037,6 +1083,22 @@ fn move_item(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(json!({ "library": id, "kind": str_param(p, "kind"), "item": key, "group": to }))
|
||||
}
|
||||
|
||||
fn move_group(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "library.moveGroup";
|
||||
let id = library_id(s, p, C)?;
|
||||
let group = group_param(p, C)?;
|
||||
let to = p.get("to").and_then(Value::as_u64).ok_or_else(|| bad(C, "missing `to` (an index)"))?;
|
||||
let (name, at) = s.libraries.change(&id, |lib| {
|
||||
let i = group_at(lib, group, C)?;
|
||||
let g = lib.groups.remove(i);
|
||||
let at = usize::try_from(to).unwrap_or(usize::MAX).min(lib.groups.len());
|
||||
let name = g.name.clone();
|
||||
lib.groups.insert(at, g);
|
||||
Ok((name, at))
|
||||
})?;
|
||||
Ok(json!({ "library": id, "group": name, "to": at }))
|
||||
}
|
||||
|
||||
fn export(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "library.export";
|
||||
let id = library_id(s, p, C)?;
|
||||
|
||||
@@ -276,6 +276,61 @@ fn items_are_grouped_moved_and_ungrouped() {
|
||||
assert_eq!(run(&mut s, "library.list", json!({}))["libraries"][0]["groups"], 1);
|
||||
}
|
||||
|
||||
/// The names of the current library's `list` ("colors", "graphics"…), in their order.
|
||||
fn order(s: &mut Session, list: &str, key: &str) -> Vec<String> {
|
||||
let got = run(s, "library.get", json!({}));
|
||||
got[list].as_array().unwrap().iter().map(|v| v[key].as_str().unwrap().to_string()).collect()
|
||||
}
|
||||
|
||||
/// Dragging in the Libraries panel: an item moves just before or after another of its kind, into
|
||||
/// that one's group or out of every group, and groups move to another place in the list.
|
||||
#[test]
|
||||
fn items_and_groups_are_reordered() {
|
||||
let mut s = session();
|
||||
run(&mut s, "library.create", json!({"name": "Brand"}));
|
||||
for (name, color) in [("Red", "#ff0000"), ("Green", "#00ff00"), ("Blue", "#0000ff"), ("Gray", "#808080")] {
|
||||
run(&mut s, "library.add", json!({"kind": "fillColor", "color": color, "name": name}));
|
||||
}
|
||||
// Before an item, and after one; on itself it stays.
|
||||
run(&mut s, "library.moveItem", json!({"kind": "fillColor", "item": "Blue", "before": "Red"}));
|
||||
assert_eq!(order(&mut s, "colors", "name"), ["Blue", "Red", "Green", "Gray"]);
|
||||
run(&mut s, "library.moveItem", json!({"kind": "fillColor", "item": "Blue", "after": "Gray"}));
|
||||
assert_eq!(order(&mut s, "colors", "name"), ["Red", "Green", "Gray", "Blue"]);
|
||||
run(&mut s, "library.moveItem", json!({"kind": "fillColor", "item": "Red", "before": "Red"}));
|
||||
assert_eq!(order(&mut s, "colors", "name"), ["Red", "Green", "Gray", "Blue"]);
|
||||
// Into a group next to an item there: the group lists it next to that one too.
|
||||
run(
|
||||
&mut s,
|
||||
"library.createGroup",
|
||||
json!({"name": "Cool", "items": [{"kind": "fillColor", "item": "Green"}, {"kind": "fillColor", "item": "Blue"}]}),
|
||||
);
|
||||
let r = run(&mut s, "library.moveItem", json!({"kind": "fillColor", "item": "Gray", "group": "Cool", "before": "Blue"}));
|
||||
assert_eq!(r["group"], "Cool");
|
||||
assert_eq!(order(&mut s, "colors", "name"), ["Red", "Green", "Gray", "Blue"]);
|
||||
assert_eq!(groups(&mut s), [("Cool".into(), refs(&[("color", "Green"), ("color", "Gray"), ("color", "Blue")]))]);
|
||||
// Next to an ungrouped item, without a group: out of its group.
|
||||
run(&mut s, "library.moveItem", json!({"kind": "fillColor", "item": "Blue", "before": "Red"}));
|
||||
assert_eq!(order(&mut s, "colors", "name"), ["Blue", "Red", "Green", "Gray"]);
|
||||
assert_eq!(groups(&mut s), [("Cool".into(), refs(&[("color", "Green"), ("color", "Gray")]))]);
|
||||
// Wrong asks change nothing.
|
||||
assert!(s.execute("library.moveItem", &json!({"kind": "fillColor", "item": "Red", "before": "Nope"})).is_err());
|
||||
assert!(s.execute("library.moveItem", &json!({"kind": "fillColor", "item": "Red", "before": "Blue", "after": "Gray"})).is_err());
|
||||
assert!(s.execute("library.moveItem", &json!({"kind": "charStyle", "item": "Red", "before": "Blue"})).is_err(), "not a style");
|
||||
assert_eq!(order(&mut s, "colors", "name"), ["Blue", "Red", "Green", "Gray"]);
|
||||
// Groups: to an index, past the end to the last place.
|
||||
run(&mut s, "library.createGroup", json!({"name": "Warm"}));
|
||||
run(&mut s, "library.createGroup", json!({"name": "Neutral"}));
|
||||
let names = |s: &mut Session| groups(s).into_iter().map(|(n, _)| n).collect::<Vec<_>>();
|
||||
assert_eq!(run(&mut s, "library.moveGroup", json!({"group": "neutral", "to": 0}))["to"], 0);
|
||||
assert_eq!(names(&mut s), ["Neutral", "Cool", "Warm"]);
|
||||
assert_eq!(run(&mut s, "library.moveGroup", json!({"group": "Neutral", "to": 99}))["to"], 2);
|
||||
assert_eq!(names(&mut s), ["Cool", "Warm", "Neutral"]);
|
||||
assert!(s.execute("library.moveGroup", &json!({"group": "Nope", "to": 0})).is_err());
|
||||
assert!(s.execute("library.moveGroup", &json!({"group": "Cool", "to": -1})).is_err());
|
||||
assert!(s.execute("library.moveGroup", &json!({"group": "Cool"})).is_err());
|
||||
assert_eq!(groups(&mut s)[0].1, refs(&[("color", "Green"), ("color", "Gray")]), "a group keeps its items when it moves");
|
||||
}
|
||||
|
||||
/// A library goes out to a file with its art and groups and comes back as a new library, numbered
|
||||
/// when its name is taken; the graphic it brings places in a document (#926).
|
||||
#[test]
|
||||
|
||||
@@ -988,7 +988,8 @@ fn styles(what: &str, data: &str) -> Result<(), TestCaseError> {
|
||||
}
|
||||
|
||||
/// Libraries panel library file `data` imported, then each kind's first item used on the rich
|
||||
/// document and moved into its first group, the group deleted and the library exported (#926).
|
||||
/// document, moved into its first group and next to its kind's last item, the group moved and
|
||||
/// deleted and the library exported (#926).
|
||||
fn libraries(what: &str, data: &str) -> Result<(), TestCaseError> {
|
||||
survive_library(what, "library.import", data, |s, r| {
|
||||
let lib = r["id"].clone();
|
||||
@@ -1001,9 +1002,17 @@ fn libraries(what: &str, data: &str) -> Result<(), TestCaseError> {
|
||||
let _ = no_panic(s.execute("library.use", &json!({"library": lib, "kind": kind, "item": item[key]})));
|
||||
let to = json!({"library": lib, "kind": kind, "item": item[key], "group": got["groups"][0]["name"]});
|
||||
let _ = no_panic(s.execute("library.moveItem", &to));
|
||||
// Next to the last item of its kind, whatever group that is in.
|
||||
let last = got[list].as_array().and_then(|a| a.last()).map(|v| v[key].clone()).unwrap_or_default();
|
||||
for side in ["before", "after"] {
|
||||
let _ = no_panic(s.execute("library.moveItem", &json!({"library": lib, "kind": kind, "item": item[key], side: last})));
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(g) = got["groups"].get(0) {
|
||||
for to in [0, 1, 1_000_000] {
|
||||
let _ = no_panic(s.execute("library.moveGroup", &json!({"library": lib, "group": g["name"], "to": to})));
|
||||
}
|
||||
let _ = no_panic(s.execute("library.deleteGroup", &json!({"library": lib, "group": g["name"]})));
|
||||
}
|
||||
let _ = no_panic(s.execute("library.export", &json!({"library": lib})));
|
||||
|
||||
@@ -2040,6 +2040,8 @@ fn panel_drop(app: &mut VectorcraftApp, ui: &Ui, resp: &egui::Response, xf: &Xf)
|
||||
// Art dragged back onto the canvas: its move was already dropped. (A symbol was placed
|
||||
// above.)
|
||||
widgets::PanelDrag::Art(_) | widgets::PanelDrag::Symbol(_) | widgets::PanelDrag::LibraryGraphic { .. } => return,
|
||||
// A Libraries panel style or group only moves within that panel.
|
||||
widgets::PanelDrag::LibraryItem { .. } | widgets::PanelDrag::LibraryGroup { .. } => return,
|
||||
};
|
||||
if let Err(e) = app.run(cmd, params) {
|
||||
app.status(e);
|
||||
@@ -6958,7 +6960,7 @@ mod tests {
|
||||
let fill = |app: &VectorcraftApp| app.session.active().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().appearance.fill_paint();
|
||||
let red = Paint::solid(Color::from_hex("#ed1c24").unwrap());
|
||||
let rows = SwatchRows { grabbed: "Red".into(), names: vec!["Red".into()], groups: false };
|
||||
let swatch = PanelDrag::Paint { paint: red.clone(), params: json!({"swatch": "Red"}), rows: Some(rows) };
|
||||
let swatch = PanelDrag::Paint { paint: red.clone(), params: json!({"swatch": "Red"}), rows: Some(rows), library: None };
|
||||
// Off the art nothing happens.
|
||||
drop(&mut app, swatch.clone(), Point::new(300.0, 250.0));
|
||||
assert_eq!(fill(&app), Paint::solid(Color::WHITE));
|
||||
|
||||
@@ -467,7 +467,11 @@ pub(crate) fn panel_body(app: &mut VectorcraftApp, ui: &mut Ui, id: &str, width:
|
||||
ui.set_max_height(tall);
|
||||
panels::layers::show(app, ui);
|
||||
}
|
||||
Some(DockTab::Libraries) => panels::libraries(app, ui),
|
||||
Some(DockTab::Libraries) => {
|
||||
// Its list scrolls inside `tall` (as in the dock) instead of growing the flyout.
|
||||
ui.set_max_height(tall);
|
||||
panels::libraries(app, ui);
|
||||
}
|
||||
None => panels::show_icon_panel(app, ui, id),
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3093,7 +3093,7 @@
|
||||
Delete Library Item Изтриване на елемент от библиотека
|
||||
No results Няма резултати
|
||||
No items in this library match “{query}”. Няма елементи в тази библиотека, съвпадащи с „{query}“.
|
||||
Right-click an item to move it into this group. Щракнете с десния бутон върху елемент, за да го преместите в тази група.
|
||||
Drag an item here, or right-click one, to put it in this group. Плъзнете елемент тук или щракнете с десния бутон върху него, за да го поставите в тази група.
|
||||
New Group Нова група
|
||||
New Group… Нова група…
|
||||
Move to Group Преместване в група
|
||||
@@ -5204,5 +5204,7 @@ theme Appearance Външен вид
|
||||
Plastic Wrap Найлоново фолио
|
||||
Chrome… Хром…
|
||||
Chrome Хром
|
||||
Drop here to take it out of its group Пуснете тук, за да го извадите от групата му
|
||||
Move Library Group Преместване на библиотечна група
|
||||
Plaster… Гипс…
|
||||
Plaster Гипс
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1657,7 +1657,7 @@ lighting Light Světlo
|
||||
This library is empty Tato knihovna je prázdná
|
||||
No results Žádné výsledky
|
||||
No items in this library match “{query}”. Žádná položka v této knihovně neodpovídá „{query}“.
|
||||
Right-click an item to move it into this group. Kliknutím pravým tlačítkem na položku ji přesunete do této skupiny.
|
||||
Drag an item here, or right-click one, to put it in this group. Přetáhněte sem položku nebo na ni klikněte pravým tlačítkem a vložte ji do této skupiny.
|
||||
New Group Nová skupina
|
||||
New Group… Nová skupina…
|
||||
Move to Group Přesunout do skupiny
|
||||
@@ -5243,5 +5243,7 @@ mark-style Japanese Japonské
|
||||
Plastic Wrap Plastová fólie
|
||||
Chrome… Chrom…
|
||||
Chrome Chrom
|
||||
Drop here to take it out of its group Pusťte sem, chcete-li ji vyjmout ze skupiny
|
||||
Move Library Group Přesunout skupinu knihovny
|
||||
Plaster… Sádra…
|
||||
Plaster Sádra
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1489,7 +1489,7 @@ lighting Light Lys
|
||||
This library is empty Dette bibliotek er tomt
|
||||
No results Ingen resultater
|
||||
No items in this library match “{query}”. Ingen elementer i dette bibliotek matcher »{query}«.
|
||||
Right-click an item to move it into this group. Højreklik på et element for at flytte det til denne gruppe.
|
||||
Drag an item here, or right-click one, to put it in this group. Træk et element hertil, eller højreklik på et, for at lægge det i denne gruppe.
|
||||
New Group Ny gruppe
|
||||
New Group… Ny gruppe…
|
||||
Move to Group Flyt til gruppe
|
||||
@@ -5222,5 +5222,7 @@ swatch Bright Violet Kraftig violet
|
||||
Plastic Wrap Plastfolie
|
||||
Chrome… Krom…
|
||||
Chrome Krom
|
||||
Drop here to take it out of its group Slip her for at tage det ud af gruppen
|
||||
Move Library Group Flyt biblioteksgruppe
|
||||
Plaster… Gips…
|
||||
Plaster Gips
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4362,7 +4362,7 @@ theme Appearance Erscheinungsbild
|
||||
Move to Group In Gruppe verschieben
|
||||
No results Keine Ergebnisse
|
||||
No items in this library match “{query}”. Keine Elemente in dieser Bibliothek entsprechen „{query}“.
|
||||
Right-click an item to move it into this group. Klicken Sie mit der rechten Maustaste auf ein Element, um es in diese Gruppe zu verschieben.
|
||||
Drag an item here, or right-click one, to put it in this group. Ziehen Sie ein Element hierher oder klicken Sie mit der rechten Maustaste darauf, um es in diese Gruppe zu legen.
|
||||
@msg Imported library {name} Bibliothek {name} importiert
|
||||
# Linked files changed on disk.
|
||||
Update Changed Links Geänderte Verknüpfungen aktualisieren
|
||||
@@ -4823,5 +4823,7 @@ effect Soft Weich
|
||||
Plastic Wrap Kunststofffolie
|
||||
Chrome… Chrom…
|
||||
Chrome Chrom
|
||||
Drop here to take it out of its group Hier ablegen, um es aus seiner Gruppe zu nehmen
|
||||
Move Library Group Bibliotheksgruppe verschieben
|
||||
Plaster… Gipsabdruck…
|
||||
Plaster Gipsabdruck
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3492,7 +3492,7 @@ theme Appearance Εμφάνιση
|
||||
Delete Library Item Διαγραφή στοιχείου βιβλιοθήκης
|
||||
No results Δεν βρέθηκαν αποτελέσματα
|
||||
No items in this library match “{query}”. Κανένα στοιχείο αυτής της βιβλιοθήκης δεν αντιστοιχεί στο «{query}».
|
||||
Right-click an item to move it into this group. Κάντε δεξί κλικ σε στοιχείο για να το μετακινήσετε σε αυτήν την ομάδα.
|
||||
Drag an item here, or right-click one, to put it in this group. Σύρετε εδώ ένα στοιχείο ή κάντε δεξί κλικ σε ένα για να το βάλετε σε αυτήν την ομάδα.
|
||||
Move to Group Μετακίνηση σε ομάδα
|
||||
No Group Χωρίς ομάδα
|
||||
Import Library… Εισαγωγή βιβλιοθήκης…
|
||||
@@ -5195,5 +5195,7 @@ effect Points Σημεία
|
||||
Plastic Wrap Πλαστική μεμβράνη
|
||||
Chrome… Χρώμιο…
|
||||
Chrome Χρώμιο
|
||||
Drop here to take it out of its group Αφήστε εδώ για να το βγάλετε από την ομάδα του
|
||||
Move Library Group Μετακίνηση ομάδας βιβλιοθήκης
|
||||
Plaster… Γύψος…
|
||||
Plaster Γύψος
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2016 and column 19.
|
@@ -3152,7 +3152,7 @@
|
||||
Delete Library Item Eliminar elemento de la biblioteca
|
||||
No results Sin resultados
|
||||
No items in this library match “{query}”. Ningún elemento de esta biblioteca coincide con “{query}”.
|
||||
Right-click an item to move it into this group. Haz clic con el botón derecho en un elemento para moverlo a este grupo.
|
||||
Drag an item here, or right-click one, to put it in this group. Arrastra aquí un elemento, o haz clic con el botón derecho en uno, para ponerlo en este grupo.
|
||||
New Group Nuevo grupo
|
||||
New Group… Nuevo grupo…
|
||||
Move to Group Mover al grupo
|
||||
@@ -5185,5 +5185,7 @@ mark-style Japanese Japonesas
|
||||
Plastic Wrap Envoltura plástica
|
||||
Chrome… Cromo…
|
||||
Chrome Cromo
|
||||
Drop here to take it out of its group Suelta aquí para sacarlo de su grupo
|
||||
Move Library Group Mover grupo de biblioteca
|
||||
Plaster… Yeso…
|
||||
Plaster Yeso
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1620,7 +1620,7 @@ lighting Light Valo
|
||||
This library is empty Tämä kirjasto on tyhjä
|
||||
No results Ei tuloksia
|
||||
No items in this library match “{query}”. Mikään tämän kirjaston kohde ei vastaa hakua ”{query}”.
|
||||
Right-click an item to move it into this group. Siirrä kohde tähän ryhmään napsauttamalla sitä hiiren kakkospainikkeella.
|
||||
Drag an item here, or right-click one, to put it in this group. Vedä kohde tähän tai napsauta sitä hiiren kakkospainikkeella lisätäksesi sen tähän ryhmään.
|
||||
New Group Uusi ryhmä
|
||||
New Group… Uusi ryhmä…
|
||||
Move to Group Siirrä ryhmään
|
||||
@@ -5274,5 +5274,7 @@ swatch Bright Violet Kirkas violetti
|
||||
Plastic Wrap Muovikalvo
|
||||
Chrome… Kromi…
|
||||
Chrome Kromi
|
||||
Drop here to take it out of its group Pudota tähän poistaaksesi sen ryhmästään
|
||||
Move Library Group Siirrä kirjastoryhmä
|
||||
Plaster… Kipsi…
|
||||
Plaster Kipsi
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3149,7 +3149,7 @@
|
||||
Delete Library Item Supprimer l’élément de la bibliothèque
|
||||
No results Aucun résultat
|
||||
No items in this library match “{query}”. Aucun élément de cette bibliothèque ne correspond à « {query} ».
|
||||
Right-click an item to move it into this group. Cliquez avec le bouton droit sur un élément pour le placer dans ce groupe.
|
||||
Drag an item here, or right-click one, to put it in this group. Faites glisser un élément ici, ou cliquez dessus avec le bouton droit, pour le placer dans ce groupe.
|
||||
New Group Nouveau groupe
|
||||
New Group… Nouveau groupe…
|
||||
Move to Group Déplacer vers le groupe
|
||||
@@ -5242,5 +5242,7 @@ theme Appearance Apparence
|
||||
Plastic Wrap Emballage plastique
|
||||
Chrome… Chrome…
|
||||
Chrome Chrome
|
||||
Drop here to take it out of its group Déposez ici pour le sortir de son groupe
|
||||
Move Library Group Déplacer le groupe de bibliothèque
|
||||
Plaster… Plâtre…
|
||||
Plaster Plâtre
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3171,7 +3171,7 @@ mark-style Japanese Japán
|
||||
Right Indent Jobb behúzás
|
||||
Right Plane Jobb sík
|
||||
Right to Left Jobbról balra
|
||||
Right-click an item to move it into this group. Egy elemet jobb gombbal kattintva helyezhetsz át ebbe a csoportba.
|
||||
Drag an item here, or right-click one, to put it in this group. Húzz ide egy elemet, vagy kattints rá jobb gombbal, hogy ebbe a csoportba kerüljön.
|
||||
Right-Justified Tab Jobbra igazító tabulátor
|
||||
Right-to-Left Paragraph Direction Jobbról balra haladó bekezdés
|
||||
Right: Jobb:
|
||||
@@ -5229,5 +5229,7 @@ imageTrace Outline Körvonal
|
||||
Plastic Wrap Fóliázás
|
||||
Chrome… Króm…
|
||||
Chrome Króm
|
||||
Drop here to take it out of its group Engedd el itt, hogy kikerüljön a csoportjából
|
||||
Move Library Group Könyvtárcsoport áthelyezése
|
||||
Plaster… Gipsz…
|
||||
Plaster Gipsz
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1489,7 +1489,7 @@ lighting Light Cahaya
|
||||
This library is empty Pustaka ini kosong
|
||||
No results Tidak ada hasil
|
||||
No items in this library match “{query}”. Tidak ada item di pustaka ini yang cocok dengan “{query}”.
|
||||
Right-click an item to move it into this group. Klik kanan item untuk memindahkannya ke grup ini.
|
||||
Drag an item here, or right-click one, to put it in this group. Seret item ke sini, atau klik kanan item, untuk memasukkannya ke grup ini.
|
||||
New Group Grup baru
|
||||
New Group… Grup baru…
|
||||
Move to Group Pindahkan ke grup
|
||||
@@ -5222,5 +5222,7 @@ swatch Bright Violet Violet cerah
|
||||
Plastic Wrap Pembungkus plastik
|
||||
Chrome… Krom…
|
||||
Chrome Krom
|
||||
Drop here to take it out of its group Lepaskan di sini untuk mengeluarkannya dari grupnya
|
||||
Move Library Group Pindahkan grup pustaka
|
||||
Plaster… Plester…
|
||||
Plaster Plester
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 3772 and column 19.
|
@@ -3185,7 +3185,7 @@ mark-style Japanese Giapponese
|
||||
Delete Library Item Elimina elemento della libreria
|
||||
No results Nessun risultato
|
||||
No items in this library match “{query}”. Nessun elemento di questa libreria corrisponde a “{query}”.
|
||||
Right-click an item to move it into this group. Fai clic con il pulsante destro su un elemento per spostarlo in questo gruppo.
|
||||
Drag an item here, or right-click one, to put it in this group. Trascina qui un elemento, o fai clic con il pulsante destro su uno, per metterlo in questo gruppo.
|
||||
New Group Nuovo gruppo
|
||||
New Group… Nuovo gruppo…
|
||||
Move to Group Sposta nel gruppo
|
||||
@@ -4857,5 +4857,7 @@ lighting Light Luce
|
||||
Plastic Wrap Pellicola plastica
|
||||
Chrome… Cromo…
|
||||
Chrome Cromo
|
||||
Drop here to take it out of its group Rilascia qui per toglierlo dal suo gruppo
|
||||
Move Library Group Sposta gruppo di libreria
|
||||
Plaster… Gesso…
|
||||
Plaster Gesso
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2917,7 +2917,7 @@ mark-style Japanese 日本式
|
||||
Delete Library Item ライブラリ項目を削除
|
||||
No results 結果はありません
|
||||
No items in this library match “{query}”. このライブラリには「{query}」に一致する項目がありません。
|
||||
Right-click an item to move it into this group. 項目を右クリックすると、このグループに移動できます。
|
||||
Drag an item here, or right-click one, to put it in this group. 項目をここにドラッグするか、項目を右クリックすると、このグループに入れられます。
|
||||
New Group 新規グループ
|
||||
New Group… 新規グループ…
|
||||
Move to Group グループに移動
|
||||
@@ -4897,5 +4897,7 @@ lighting Light ライト
|
||||
Plastic Wrap ラップ
|
||||
Chrome… クロム…
|
||||
Chrome クロム
|
||||
Drop here to take it out of its group ここにドロップするとグループから外れます
|
||||
Move Library Group ライブラリグループを移動
|
||||
Plaster… 石膏…
|
||||
Plaster 石膏
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1398,7 +1398,7 @@ lighting Light სინათლე
|
||||
This library is empty ეს ბიბლიოთეკა ცარიელია
|
||||
No results შედეგები არ არის
|
||||
No items in this library match “{query}”. ამ ბიბლიოთეკაში „{query}“-ს შესაბამისი ელემენტი არ არის.
|
||||
Right-click an item to move it into this group. ამ ჯგუფში გადასატანად დაწკაპეთ ელემენტზე მარჯვენა ღილაკით.
|
||||
Drag an item here, or right-click one, to put it in this group. ამ ჯგუფში ჩასასმელად ჩაავლეთ ელემენტი აქ ან დაწკაპეთ მასზე მარჯვენა ღილაკით.
|
||||
New Group ახალი ჯგუფი
|
||||
New Group… ახალი ჯგუფი…
|
||||
Move to Group ჯგუფში გადატანა
|
||||
@@ -5150,5 +5150,7 @@ imageTrace Outline კონტური
|
||||
Plastic Wrap პლასტიკური აფსკი
|
||||
Chrome… ქრომი…
|
||||
Chrome ქრომი
|
||||
Drop here to take it out of its group ჯგუფიდან ამოსაღებად ჩააგდეთ აქ
|
||||
Move Library Group ბიბლიოთეკის ჯგუფის გადატანა
|
||||
Plaster… თაბაშირი…
|
||||
Plaster თაბაშირი
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2911,7 +2911,7 @@ mark-style Japanese 일본식
|
||||
Delete Library Item 라이브러리 항목 삭제
|
||||
No results 결과 없음
|
||||
No items in this library match “{query}”. 이 라이브러리에 “{query}”와(과) 일치하는 항목이 없습니다.
|
||||
Right-click an item to move it into this group. 항목을 마우스 오른쪽 버튼으로 클릭해 이 그룹으로 이동하세요.
|
||||
Drag an item here, or right-click one, to put it in this group. 항목을 여기로 끌어 놓거나 마우스 오른쪽 버튼으로 클릭해 이 그룹에 넣으세요.
|
||||
New Group 새 그룹
|
||||
New Group… 새 그룹…
|
||||
Move to Group 그룹으로 이동
|
||||
@@ -4820,5 +4820,7 @@ lighting Light 광원
|
||||
Plastic Wrap 비닐 랩
|
||||
Chrome… 크롬…
|
||||
Chrome 크롬
|
||||
Drop here to take it out of its group 그룹에서 빼려면 여기에 놓으세요
|
||||
Move Library Group 라이브러리 그룹 이동
|
||||
Plaster… 석고…
|
||||
Plaster 석고
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1503,7 +1503,7 @@ lighting Light Licht
|
||||
This library is empty Deze bibliotheek is leeg
|
||||
No results Geen resultaten
|
||||
No items in this library match “{query}”. Geen items in deze bibliotheek komen overeen met „{query}”.
|
||||
Right-click an item to move it into this group. Klik met de rechtermuisknop op een item om het naar deze groep te verplaatsen.
|
||||
Drag an item here, or right-click one, to put it in this group. Sleep een item hierheen, of klik er met de rechtermuisknop op, om het in deze groep te zetten.
|
||||
New Group Nieuwe groep
|
||||
New Group… Nieuwe groep…
|
||||
Move to Group Verplaatsen naar groep
|
||||
@@ -5264,5 +5264,7 @@ swatch Bright Violet Felviolet
|
||||
Plastic Wrap Plasticfolie
|
||||
Chrome… Chroom…
|
||||
Chrome Chroom
|
||||
Drop here to take it out of its group Zet het hier neer om het uit de groep te halen
|
||||
Move Library Group Bibliotheekgroep verplaatsen
|
||||
Plaster… Gips…
|
||||
Plaster Gips
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1577,7 +1577,7 @@ lighting Light Światło
|
||||
This library is empty Ta biblioteka jest pusta
|
||||
No results Brak wyników
|
||||
No items in this library match “{query}”. Żaden element tej biblioteki nie pasuje do „{query}”.
|
||||
Right-click an item to move it into this group. Kliknij element prawym przyciskiem, aby przenieść go do tej grupy.
|
||||
Drag an item here, or right-click one, to put it in this group. Przeciągnij tu element albo kliknij go prawym przyciskiem, aby umieścić go w tej grupie.
|
||||
New Group Nowa grupa
|
||||
New Group… Nowa grupa…
|
||||
Move to Group Przenieś do grupy
|
||||
@@ -5095,5 +5095,7 @@ mark-style Japanese Japońskie
|
||||
Plastic Wrap Folia plastikowa
|
||||
Chrome… Chrom…
|
||||
Chrome Chrom
|
||||
Drop here to take it out of its group Upuść tutaj, aby wyjąć go z grupy
|
||||
Move Library Group Przenieś grupę biblioteki
|
||||
Plaster… Gips…
|
||||
Plaster Gips
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3242,7 +3242,7 @@ lighting Light Luz
|
||||
Delete Library Item Excluir item da biblioteca
|
||||
No results Nenhum resultado
|
||||
No items in this library match “{query}”. Nenhum item desta biblioteca corresponde a “{query}”.
|
||||
Right-click an item to move it into this group. Clique com o botão direito em um item para movê-lo para este grupo.
|
||||
Drag an item here, or right-click one, to put it in this group. Arraste um item para cá, ou clique com o botão direito em um, para colocá-lo neste grupo.
|
||||
New Group Novo grupo
|
||||
Move to Group Mover para o grupo
|
||||
Import Library Importar biblioteca
|
||||
@@ -5160,5 +5160,7 @@ mark-style Japanese Japonesas
|
||||
Plastic Wrap Envoltório plástico
|
||||
Chrome… Cromo…
|
||||
Chrome Cromo
|
||||
Drop here to take it out of its group Solte aqui para tirá-lo do grupo
|
||||
Move Library Group Mover grupo de biblioteca
|
||||
Plaster… Gesso…
|
||||
Plaster Gesso
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -3181,7 +3181,7 @@ mark-style Japanese Японский
|
||||
Delete Library Item Удалить элемент библиотеки
|
||||
No results Ничего не найдено
|
||||
No items in this library match “{query}”. В этой библиотеке нет элементов, соответствующих «{query}».
|
||||
Right-click an item to move it into this group. Щёлкните элемент правой кнопкой мыши, чтобы переместить его в эту группу.
|
||||
Drag an item here, or right-click one, to put it in this group. Перетащите сюда элемент или щёлкните его правой кнопкой мыши, чтобы поместить его в эту группу.
|
||||
New Group Новая группа
|
||||
New Group… Новая группа…
|
||||
Move to Group Переместить в группу
|
||||
@@ -4853,5 +4853,7 @@ lighting Light Свет
|
||||
Plastic Wrap Целлофановая упаковка
|
||||
Chrome… Хром…
|
||||
Chrome Хром
|
||||
Drop here to take it out of its group Отпустите здесь, чтобы вынуть его из группы
|
||||
Move Library Group Переместить группу библиотеки
|
||||
Plaster… Гипс…
|
||||
Plaster Гипс
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3093,7 +3093,7 @@
|
||||
Delete Library Item Radera biblioteksobjekt
|
||||
No results Inga resultat
|
||||
No items in this library match “{query}”. Inga objekt i det här biblioteket matchar ”{query}”.
|
||||
Right-click an item to move it into this group. Högerklicka på ett objekt för att flytta det till den här gruppen.
|
||||
Drag an item here, or right-click one, to put it in this group. Dra hit ett objekt, eller högerklicka på ett, för att lägga det i den här gruppen.
|
||||
New Group Ny grupp
|
||||
New Group… Ny grupp…
|
||||
Move to Group Flytta till grupp
|
||||
@@ -5199,5 +5199,7 @@ theme Appearance Utseende
|
||||
Plastic Wrap Plastfolie
|
||||
Chrome… Krom…
|
||||
Chrome Krom
|
||||
Drop here to take it out of its group Släpp här för att ta ut det ur gruppen
|
||||
Move Library Group Flytta biblioteksgrupp
|
||||
Plaster… Gips…
|
||||
Plaster Gips
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -3178,7 +3178,7 @@ effect Points Точки
|
||||
Delete Library Item Видалити елемент бібліотеки
|
||||
No results Нічого не знайдено
|
||||
No items in this library match “{query}”. У цій бібліотеці немає елементів, що відповідають «{query}».
|
||||
Right-click an item to move it into this group. Клацніть елемент правою кнопкою миші, щоб перемістити його в цю групу.
|
||||
Drag an item here, or right-click one, to put it in this group. Перетягніть сюди елемент або клацніть його правою кнопкою миші, щоб покласти його в цю групу.
|
||||
New Group Нова група
|
||||
New Group… Нова група…
|
||||
Move to Group Перемістити в групу
|
||||
@@ -4837,5 +4837,7 @@ lighting Light Світло
|
||||
Plastic Wrap Целофанова плівка
|
||||
Chrome… Хром…
|
||||
Chrome Хром
|
||||
Drop here to take it out of its group Відпустіть тут, щоб вийняти його з групи
|
||||
Move Library Group Перемістити групу бібліотеки
|
||||
Plaster… Гіпс…
|
||||
Plaster Гіпс
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 725 and column 40.
|
@@ -2926,7 +2926,7 @@ mark-style Japanese Kiểu Nhật
|
||||
Right Indent Thụt lề phải
|
||||
Right Plane Mặt phẳng phải
|
||||
Right to Left Phải sang trái
|
||||
Right-click an item to move it into this group. Bấm chuột phải vào mục để di chuyển vào nhóm này.
|
||||
Drag an item here, or right-click one, to put it in this group. Kéo một mục vào đây, hoặc bấm chuột phải vào mục, để đưa vào nhóm này.
|
||||
Right-Justified Tab Tab căn phải
|
||||
Right-to-Left Paragraph Direction Hướng đoạn văn từ phải sang trái
|
||||
Right: Phải:
|
||||
@@ -4837,5 +4837,7 @@ theme Appearance Giao diện
|
||||
Plastic Wrap Màng bọc nhựa
|
||||
Chrome… Crôm…
|
||||
Chrome Crôm
|
||||
Drop here to take it out of its group Thả vào đây để đưa ra khỏi nhóm
|
||||
Move Library Group Di chuyển nhóm thư viện
|
||||
Plaster… Thạch cao…
|
||||
Plaster Thạch cao
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 3962 and column 10.
|
@@ -3036,7 +3036,7 @@ font-script Japanese 汉字
|
||||
Delete Library Item 删除库项目
|
||||
No results 无结果
|
||||
No items in this library match “{query}”. 此库中没有与“{query}”匹配的项目。
|
||||
Right-click an item to move it into this group. 右键单击项目可将其移到此组。
|
||||
Drag an item here, or right-click one, to put it in this group. 将项目拖到此处,或右键单击项目,即可将其放入此组。
|
||||
New Group 新建组
|
||||
New Group… 新建组…
|
||||
Move to Group 移到组
|
||||
@@ -5080,5 +5080,7 @@ lighting Light 光源
|
||||
Plastic Wrap 塑料包装
|
||||
Chrome… 铬黄…
|
||||
Chrome 铬黄
|
||||
Drop here to take it out of its group 放到此处可将其移出所在组
|
||||
Move Library Group 移动库组
|
||||
Plaster… 塑料效果…
|
||||
Plaster 塑料效果
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3904,7 +3904,7 @@ font-script Japanese 漢字
|
||||
Delete Library Item 刪除資料庫項目
|
||||
No results 沒有結果
|
||||
No items in this library match “{query}”. 此資料庫中沒有符合「{query}」的項目。
|
||||
Right-click an item to move it into this group. 在項目上按一下右鍵,即可將其移至此群組。
|
||||
Drag an item here, or right-click one, to put it in this group. 將項目拖到此處,或在項目上按一下右鍵,即可將其放入此群組。
|
||||
New Group 新增群組
|
||||
New Group… 新增群組…
|
||||
Move to Group 移至群組
|
||||
@@ -4875,5 +4875,7 @@ lighting Light 光源
|
||||
Plastic Wrap 塑膠包裝
|
||||
Chrome… 鉻黃…
|
||||
Chrome 鉻黃
|
||||
Drop here to take it out of its group 放到此處即可將其移出所屬群組
|
||||
Move Library Group 移動資料庫群組
|
||||
Plaster… 塑膠效果…
|
||||
Plaster 塑膠效果
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -1044,7 +1044,7 @@ mod tests {
|
||||
// A spot swatch tile dropped on a stop links it: editing the swatch recolours the stop.
|
||||
app.run("swatch.new", json!({"name": "Ink", "color": "#cc0066", "spot": true})).unwrap();
|
||||
let ink = app.session.doc().unwrap().doc.swatch("Ink").unwrap().paint.clone();
|
||||
egui::DragAndDrop::set_payload(&p.ctx, PanelDrag::Paint { paint: ink, params: json!({"swatch": "Ink"}), rows: None });
|
||||
egui::DragAndDrop::set_payload(&p.ctx, PanelDrag::Paint { paint: ink, params: json!({"swatch": "Ink"}), rows: None, library: None });
|
||||
p.frame(&mut app, vec![Event::PointerMoved(a)], Modifiers::NONE);
|
||||
p.frame(
|
||||
&mut app,
|
||||
|
||||
@@ -351,7 +351,12 @@ fn zone_input(ui: &Ui, zone: &Response, d: &Document, ev: &mut Events) {
|
||||
PanelDrag::GraphicStyle(n) => (Some(n), None),
|
||||
PanelDrag::Art(ids) => (None, ids.first().copied()),
|
||||
PanelDrag::Appearance(id) => (None, Some(*id)),
|
||||
PanelDrag::Paint { .. } | PanelDrag::Symbol(_) | PanelDrag::Brush { .. } | PanelDrag::LibraryGraphic { .. } => (None, None),
|
||||
PanelDrag::Paint { .. }
|
||||
| PanelDrag::Symbol(_)
|
||||
| PanelDrag::Brush { .. }
|
||||
| PanelDrag::LibraryGraphic { .. }
|
||||
| PanelDrag::LibraryItem { .. }
|
||||
| PanelDrag::LibraryGroup { .. } => (None, None),
|
||||
};
|
||||
if style.is_none() && source.is_none() {
|
||||
return;
|
||||
|
||||
@@ -8,6 +8,12 @@
|
||||
//! The search field filters the items by name; items can be moved into user-named groups, shown as
|
||||
//! collapsible sections after the ungrouped items; and the panel menu exports the library to a
|
||||
//! `.vclibrary` file and imports one as a new library (#926).
|
||||
//!
|
||||
//! The list fills the panel down to its bottom bar and scrolls (also while a drag nears its top or
|
||||
//! bottom edge). An item dragged within it lands before or after another of its kind, in that one's
|
||||
//! group (`library.moveItem` with `before`/`after`), or, dropped elsewhere on a group or on the
|
||||
//! ungrouped items, goes into that group or out of its own; a group dragged by its header goes
|
||||
//! before or after another group (`library.moveGroup`).
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -24,6 +30,13 @@ use crate::{VectorcraftApp, icons};
|
||||
const TILE: f32 = 56.0;
|
||||
/// The size of a colour chip.
|
||||
const CHIP: f32 = 22.0;
|
||||
/// The height under the list the bottom bar takes (its divider and buttons) until it has been
|
||||
/// measured.
|
||||
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)> {
|
||||
@@ -75,6 +88,25 @@ enum Act {
|
||||
RenameGroup(String),
|
||||
}
|
||||
|
||||
/// What a drag in the panel holds of the library shown: one of its items, or one of its groups
|
||||
/// (by the group's header).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
enum Grab {
|
||||
Item(ItemKind, String),
|
||||
Group(String),
|
||||
}
|
||||
|
||||
/// The item or group of library `library` that `drag` holds, if it holds one.
|
||||
fn grabbed(drag: &PanelDrag, library: &str) -> Option<Grab> {
|
||||
match drag {
|
||||
PanelDrag::Paint { library: Some((l, item)), .. } if l == library => Some(Grab::Item(ItemKind::Color, item.clone())),
|
||||
PanelDrag::LibraryGraphic { library: l, item } if l == library => Some(Grab::Item(ItemKind::Graphic, item.clone())),
|
||||
PanelDrag::LibraryItem { library: l, kind, item } if l == library => Some(Grab::Item(*kind, item.clone())),
|
||||
PanelDrag::LibraryGroup { library: l, group } if l == library => Some(Grab::Group(group.clone())),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// What the item list draws from.
|
||||
struct Items<'a> {
|
||||
library: &'a str,
|
||||
@@ -82,6 +114,8 @@ struct Items<'a> {
|
||||
/// The group of each grouped item ([`Library::group_index`]).
|
||||
groups: HashMap<(ItemKind, &'a str), usize>,
|
||||
fill_active: bool,
|
||||
/// What a drag in progress holds of this library.
|
||||
grab: Option<Grab>,
|
||||
}
|
||||
|
||||
impl Items<'_> {
|
||||
@@ -121,7 +155,8 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let query = query.trim().to_lowercase();
|
||||
let searching = !query.is_empty();
|
||||
let shown = |name: &str| !searching || name.to_lowercase().contains(&query);
|
||||
let items = Items { library: &lib_id, lib, groups: lib.group_index(), fill_active };
|
||||
let grab = egui::DragAndDrop::payload::<PanelDrag>(ui.ctx()).and_then(|d| grabbed(&d, &lib_id));
|
||||
let items = Items { library: &lib_id, lib, groups: lib.group_index(), fill_active, grab };
|
||||
// The items shown in each section: the ungrouped ones first, then each group's.
|
||||
let mut counts = vec![0usize; lib.groups.len() + 1];
|
||||
for (kind, key, _) in lib.items().filter(|(_, _, name)| shown(name)) {
|
||||
@@ -130,76 +165,113 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
}
|
||||
}
|
||||
ui.add_space(4.0);
|
||||
// The list takes the panel's height down to the bottom bar (as tall as it was last frame), and
|
||||
// scrolls.
|
||||
let bar_id = egui::Id::new("library-bottom-bar");
|
||||
let bar: f32 = ui.data(|d| d.get_temp(bar_id)).unwrap_or(BAR);
|
||||
let list_h = ui.available_height() - bar;
|
||||
let list_h = if list_h.is_finite() { list_h.max(MIN_LIST) } else { MIN_LIST };
|
||||
// Each section's rows: the ungrouped items (None), then each group (its index), header included.
|
||||
let mut sections: Vec<(Option<usize>, Rect)> = vec![];
|
||||
// Whether an item takes the drag in progress (dropped next to it), so its section doesn't.
|
||||
let mut over_item = false;
|
||||
let list_rect = widgets::list_box(ui, |ui| {
|
||||
ui.set_min_height(140.0);
|
||||
ui.set_width(ui.available_width());
|
||||
if empty {
|
||||
super::empty_state(
|
||||
ui,
|
||||
"library",
|
||||
tl!("This library is empty"),
|
||||
tl!("Drag art or swatches here, or use + to add the selection's colors and text styles."),
|
||||
);
|
||||
return ui.min_rect();
|
||||
}
|
||||
if searching && counts.iter().all(|n| *n == 0) {
|
||||
let body = crate::i18n::fmt(tl!("No items in this library match “{query}”."), &[("query", &query)]);
|
||||
super::empty_state(ui, "search", tl!("No results"), &body);
|
||||
return ui.min_rect();
|
||||
}
|
||||
egui::Frame::NONE.inner_margin(egui::Margin::same(6)).show(ui, |ui| {
|
||||
items.show(ui, &mut act, |kind, key, name| items.group_of(kind, key).is_none() && shown(name));
|
||||
for (i, g) in lib.groups.iter().enumerate() {
|
||||
let n = counts.get(i + 1).copied().unwrap_or_default();
|
||||
if searching && n == 0 {
|
||||
continue;
|
||||
}
|
||||
// Open unless closed; open while searching, to show what matched.
|
||||
let key = format!("library-group-closed:{lib_id}:{}", g.name);
|
||||
let closed: bool = super::pstate(ui.ctx(), &key);
|
||||
let open = searching || !closed;
|
||||
let header = widgets::section_toggle_name(ui, &g.name, open);
|
||||
ui.painter().text(
|
||||
header.rect.right_center() - vec2(6.0, 0.0),
|
||||
egui::Align2::RIGHT_CENTER,
|
||||
n.to_string(),
|
||||
egui::FontId::proportional(11.5),
|
||||
t.text_dim,
|
||||
let out = egui::ScrollArea::vertical().id_salt("library-scroll").max_height(list_h).auto_shrink([false, false]).show(ui, |ui| {
|
||||
ui.set_width(ui.available_width());
|
||||
if empty {
|
||||
super::empty_state(
|
||||
ui,
|
||||
"library",
|
||||
tl!("This library is empty"),
|
||||
tl!("Drag art or swatches here, or use + to add the selection's colors and text styles."),
|
||||
);
|
||||
if header.clicked() && !searching {
|
||||
super::set_pstate(ui.ctx(), &key, !closed);
|
||||
return;
|
||||
}
|
||||
if searching && counts.iter().all(|n| *n == 0) {
|
||||
let body = crate::i18n::fmt(tl!("No items in this library match “{query}”."), &[("query", &query)]);
|
||||
super::empty_state(ui, "search", tl!("No results"), &body);
|
||||
return;
|
||||
}
|
||||
egui::Frame::NONE.inner_margin(egui::Margin::same(6)).show(ui, |ui| {
|
||||
let span = |ui: &Ui, top: f32| Rect::from_x_y_ranges(ui.max_rect().x_range(), top..=ui.cursor().top());
|
||||
let top = ui.cursor().top();
|
||||
if counts.first().is_some_and(|n| *n > 0) {
|
||||
items.show(ui, &mut act, &mut over_item, |kind, key, name| items.group_of(kind, key).is_none() && shown(name));
|
||||
} else if let Some(Grab::Item(kind, item)) = &items.grab
|
||||
&& items.group_of(*kind, item).is_some()
|
||||
{
|
||||
no_group_strip(ui);
|
||||
}
|
||||
header.context_menu(|ui| {
|
||||
if menu_item(ui, "Rename Group…", true, false) {
|
||||
act = Some(Act::RenameGroup(g.name.clone()));
|
||||
ui.close();
|
||||
sections.push((None, span(ui, top)));
|
||||
for (i, g) in lib.groups.iter().enumerate() {
|
||||
let n = counts.get(i + 1).copied().unwrap_or_default();
|
||||
if searching && n == 0 {
|
||||
continue;
|
||||
}
|
||||
if menu_item(ui, "Delete Group", true, false) {
|
||||
act = Some(Act::Run("library.deleteGroup", json!({ "group": g.name })));
|
||||
ui.close();
|
||||
let top = ui.cursor().top();
|
||||
// Open unless closed; open while searching, to show what matched.
|
||||
let key = format!("library-group-closed:{lib_id}:{}", g.name);
|
||||
let closed: bool = super::pstate(ui.ctx(), &key);
|
||||
let open = searching || !closed;
|
||||
// The header also drags the group to another place in the list.
|
||||
let header = widgets::section_toggle_name(ui, &g.name, open).interact(Sense::drag());
|
||||
widgets::drag_source(ui, &header, || PanelDrag::LibraryGroup { library: lib_id.clone(), group: g.name.clone() });
|
||||
ui.painter().text(
|
||||
header.rect.right_center() - vec2(6.0, 0.0),
|
||||
egui::Align2::RIGHT_CENTER,
|
||||
n.to_string(),
|
||||
egui::FontId::proportional(11.5),
|
||||
t.text_dim,
|
||||
);
|
||||
if header.clicked() && !searching {
|
||||
super::set_pstate(ui.ctx(), &key, !closed);
|
||||
}
|
||||
});
|
||||
if !open {
|
||||
continue;
|
||||
}
|
||||
ui.add_space(2.0);
|
||||
if n == 0 {
|
||||
ui.label(egui::RichText::new(tl!("Right-click an item to move it into this group.")).size(11.5).color(t.text_dim));
|
||||
ui.add_space(6.0);
|
||||
} else {
|
||||
items.show(ui, &mut act, |kind, key, name| items.group_of(kind, key) == Some(i) && shown(name));
|
||||
header.context_menu(|ui| {
|
||||
if menu_item(ui, "Rename Group…", true, false) {
|
||||
act = Some(Act::RenameGroup(g.name.clone()));
|
||||
ui.close();
|
||||
}
|
||||
if menu_item(ui, "Delete Group", true, false) {
|
||||
act = Some(Act::Run("library.deleteGroup", json!({ "group": g.name })));
|
||||
ui.close();
|
||||
}
|
||||
});
|
||||
if open {
|
||||
ui.add_space(2.0);
|
||||
if n == 0 {
|
||||
ui.label(
|
||||
egui::RichText::new(tl!("Drag an item here, or right-click one, to put it in this group."))
|
||||
.size(11.5)
|
||||
.color(t.text_dim),
|
||||
);
|
||||
ui.add_space(6.0);
|
||||
} else {
|
||||
items.show(ui, &mut act, &mut over_item, |kind, key, name| items.group_of(kind, key) == Some(i) && shown(name));
|
||||
}
|
||||
}
|
||||
sections.push((Some(i), span(ui, top)));
|
||||
}
|
||||
});
|
||||
if items.grab.is_some() {
|
||||
scroll_at_edges(ui);
|
||||
}
|
||||
});
|
||||
ui.min_rect()
|
||||
out.inner_rect
|
||||
});
|
||||
// Art dragged off the canvas, or a swatch, dropped on the list is added to the library.
|
||||
if let Some(grab) = &items.grab
|
||||
&& !over_item
|
||||
{
|
||||
section_drop(ui, &items, grab, §ions, list_rect, &mut act);
|
||||
}
|
||||
// Art dragged off the canvas, or a swatch (not one of this library's colours), dropped on the
|
||||
// list is added to the library.
|
||||
let zone = ui.interact(list_rect, ui.id().with("libraries-drop"), Sense::hover());
|
||||
if let Some(ids) = widgets::art_drop(ui, &zone) {
|
||||
act = Some(Act::Run("library.add", json!({ "kind": "graphic", "ids": ids })));
|
||||
}
|
||||
if let Some(drag) = zone.dnd_hover_payload::<PanelDrag>()
|
||||
&& let PanelDrag::Paint { paint: Paint::Solid { color, .. }, .. } = &*drag
|
||||
&& let PanelDrag::Paint { paint: Paint::Solid { color, .. }, library, .. } = &*drag
|
||||
&& library.as_ref().is_none_or(|(l, _)| *l != lib_id)
|
||||
{
|
||||
ui.painter().rect_stroke(zone.rect, 0.0, Stroke::new(1.5, t.accent), StrokeKind::Inside);
|
||||
let color = *color;
|
||||
@@ -207,6 +279,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
act = Some(Act::Run("library.add", json!({ "kind": "fillColor", "color": color })));
|
||||
}
|
||||
}
|
||||
let bar_top = ui.cursor().top();
|
||||
widgets::bottom_bar(ui, |ui| {
|
||||
if widgets::icon_button(ui, "group", tl!("New Group"), false, 24.0).clicked() {
|
||||
act = Some(Act::NewGroup(json!([])));
|
||||
@@ -227,6 +300,10 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
}
|
||||
});
|
||||
});
|
||||
let bar_h = ui.min_rect().bottom() - bar_top;
|
||||
if bar_h.is_finite() && bar_h > 0.0 {
|
||||
ui.data_mut(|d| d.insert_temp(bar_id, bar_h));
|
||||
}
|
||||
match act {
|
||||
Some(Act::Run(cmd, mut p)) => {
|
||||
if cmd != "library.setCurrent" {
|
||||
@@ -248,8 +325,9 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
|
||||
impl Items<'_> {
|
||||
/// The colours, character and paragraph styles and graphics `keep` keeps (given the kind, key
|
||||
/// and name of each), each kind under its heading.
|
||||
fn show(&self, ui: &mut Ui, act: &mut Option<Act>, keep: impl Fn(ItemKind, &str, &str) -> bool) {
|
||||
/// and name of each), each kind under its heading. Each drags to another place in the list;
|
||||
/// `over` is set while one of them takes the drag in progress.
|
||||
fn show(&self, ui: &mut Ui, act: &mut Option<Act>, over: &mut bool, keep: impl Fn(ItemKind, &str, &str) -> bool) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let lib = self.lib;
|
||||
let colors: Vec<_> = lib.colors.iter().filter(|c| keep(ItemKind::Color, &c.name, &c.name)).collect();
|
||||
@@ -262,7 +340,13 @@ impl Items<'_> {
|
||||
let paint = Paint::solid(c.color);
|
||||
widgets::swatch_tile(ui, r, &paint, false, false);
|
||||
ui.painter().rect_stroke(r, 0.0, Stroke::new(1.0, t.border), StrokeKind::Inside);
|
||||
widgets::drag_source(ui, &resp, || PanelDrag::Paint { paint: paint.clone(), params: json!({ "color": c.color }), rows: None });
|
||||
widgets::drag_source(ui, &resp, || PanelDrag::Paint {
|
||||
paint: paint.clone(),
|
||||
params: json!({ "color": c.color }),
|
||||
rows: None,
|
||||
library: Some((self.library.to_string(), c.name.clone())),
|
||||
});
|
||||
self.drop_next_to(ui, &resp, ItemKind::Color, &c.name, false, act, over);
|
||||
let resp = resp.on_hover_text(&c.name);
|
||||
if resp.clicked() {
|
||||
let to = if self.fill_active { "fill" } else { "stroke" };
|
||||
@@ -282,13 +366,15 @@ impl Items<'_> {
|
||||
}
|
||||
heading(ui, title);
|
||||
for st in list {
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 22.0), Sense::click());
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 22.0), Sense::click_and_drag());
|
||||
if resp.hovered() {
|
||||
ui.painter().rect_filled(r, 0.0, t.hover);
|
||||
}
|
||||
let icon = if kind == ItemKind::ParaStyle { "pilcrow" } else { "type" };
|
||||
icons::paint(ui, icon, Rect::from_center_size(r.left_center() + vec2(10.0, 0.0), vec2(14.0, 14.0)), t.icon);
|
||||
ui.painter().text(r.left_center() + vec2(24.0, 0.0), egui::Align2::LEFT_CENTER, &st.name, egui::FontId::proportional(12.5), t.text);
|
||||
widgets::drag_source(ui, &resp, || PanelDrag::LibraryItem { library: self.library.to_string(), kind, item: st.name.clone() });
|
||||
self.drop_next_to(ui, &resp, kind, &st.name, true, act, over);
|
||||
let resp = resp.on_hover_text(tl!("Click to apply to the selected type"));
|
||||
if resp.clicked() {
|
||||
*act = Some(Act::Run("library.use", json!({ "kind": kind_param(kind), "item": st.name })));
|
||||
@@ -308,6 +394,7 @@ impl Items<'_> {
|
||||
ui.painter().rect_stroke(r, 0.0, Stroke::new(1.0, if resp.hovered() { t.accent } else { t.border }), StrokeKind::Inside);
|
||||
// Dragged onto the canvas, a copy is placed where it is dropped.
|
||||
widgets::drag_source(ui, &resp, || PanelDrag::LibraryGraphic { library: self.library.to_string(), item: g.id.clone() });
|
||||
self.drop_next_to(ui, &resp, ItemKind::Graphic, &g.id, false, act, over);
|
||||
let resp = resp.on_hover_text(&g.name);
|
||||
let place = || Act::Run("library.use", json!({ "kind": "graphic", "item": g.id }));
|
||||
if resp.double_clicked() {
|
||||
@@ -326,6 +413,41 @@ impl Items<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// An item of `kind` dragged over item `key`'s cell `resp` lands just before or after it, by
|
||||
/// the pointer's half (`row`: rows stack downwards; else chips and tiles run across), in its
|
||||
/// group: an accent line shows where, and `over` is set. Dropped on itself it stays.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn drop_next_to(&self, ui: &Ui, resp: &egui::Response, kind: ItemKind, key: &str, row: bool, act: &mut Option<Act>, over: &mut bool) {
|
||||
let Some(Grab::Item(grabbed_kind, moving)) = &self.grab else { return };
|
||||
if *grabbed_kind != kind || resp.dnd_hover_payload::<PanelDrag>().is_none() {
|
||||
return;
|
||||
}
|
||||
*over = true;
|
||||
if moving == key {
|
||||
resp.dnd_release_payload::<PanelDrag>();
|
||||
return;
|
||||
}
|
||||
let r = resp.rect;
|
||||
let at = ui.input(|i| i.pointer.interact_pos()).unwrap_or(r.center());
|
||||
let after = if row { at.y > r.center().y } else { at.x > r.center().x };
|
||||
let accent = Stroke::new(2.0, Tokens::get(ui.ctx()).accent);
|
||||
if row {
|
||||
let y = if after { r.bottom() } else { r.top() };
|
||||
ui.painter().line_segment([egui::pos2(r.left(), y), egui::pos2(r.right(), y)], accent);
|
||||
} else {
|
||||
let x = if after { r.right() + 2.0 } else { r.left() - 2.0 };
|
||||
ui.painter().line_segment([egui::pos2(x, r.top()), egui::pos2(x, r.bottom())], accent);
|
||||
}
|
||||
if resp.dnd_release_payload::<PanelDrag>().is_some() {
|
||||
let mut p = json!({ "kind": kind_param(kind), "item": moving });
|
||||
p[if after { "after" } else { "before" }] = json!(key);
|
||||
if let Some(g) = self.group_of(kind, key).and_then(|i| self.lib.groups.get(i)) {
|
||||
p["group"] = json!(g.name);
|
||||
}
|
||||
*act = Some(Act::Run("library.moveItem", p));
|
||||
}
|
||||
}
|
||||
|
||||
/// An item's context menu entries: Move to Group (No Group, the library's groups, New
|
||||
/// Group…) and Delete.
|
||||
fn item_menu(&self, ui: &mut Ui, act: &mut Option<Act>, kind: ItemKind, key: &str) {
|
||||
@@ -371,6 +493,82 @@ fn kind_param(kind: ItemKind) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
/// Where an item dragged out of its group can be dropped when no ungrouped item is showing.
|
||||
fn no_group_strip(ui: &mut Ui) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let (r, _) = ui.allocate_exact_size(vec2(ui.available_width(), 28.0), Sense::hover());
|
||||
ui.painter().rect_stroke(r, 3.0, Stroke::new(1.0, t.border), StrokeKind::Inside);
|
||||
ui.painter().text(
|
||||
r.center(),
|
||||
egui::Align2::CENTER_CENTER,
|
||||
tl!("Drop here to take it out of its group"),
|
||||
egui::FontId::proportional(11.5),
|
||||
t.text_dim,
|
||||
);
|
||||
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), the section outlined; a group goes before or after that group, by the
|
||||
/// pointer's half, an accent line showing where.
|
||||
fn section_drop(ui: &Ui, items: &Items, grab: &Grab, sections: &[(Option<usize>, Rect)], clip: Rect, act: &mut Option<Act>) {
|
||||
// By the pointer, not by hover zones: the headers and items over the sections take the hover.
|
||||
let Some(pos) = ui.ctx().pointer_latest_pos() else { return };
|
||||
let released = ui.input(|i| i.pointer.any_released());
|
||||
let take = || egui::DragAndDrop::take_payload::<PanelDrag>(ui.ctx()).is_some();
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let Some((group, rect)) = sections.iter().find(|(_, rect)| rect.intersect(clip).contains(pos)) else { return };
|
||||
let r = rect.intersect(clip);
|
||||
let name = |i: &usize| items.lib.groups.get(*i).map(|g| g.name.clone());
|
||||
match grab {
|
||||
Grab::Item(kind, item) => {
|
||||
if items.group_of(*kind, item) == *group {
|
||||
return;
|
||||
}
|
||||
ui.painter().rect_stroke(r, 3.0, Stroke::new(1.5, t.accent), StrokeKind::Inside);
|
||||
if released && take() {
|
||||
let mut p = json!({ "kind": kind_param(*kind), "item": item });
|
||||
if let Some(g) = group.as_ref().and_then(name) {
|
||||
p["group"] = json!(g);
|
||||
}
|
||||
*act = Some(Act::Run("library.moveItem", p));
|
||||
}
|
||||
}
|
||||
Grab::Group(moving) => {
|
||||
// Groups only go among the groups; dropped on itself, one stays.
|
||||
let (Some(to), Some(from)) = (*group, items.lib.groups.iter().position(|g| g.name == *moving)) else { return };
|
||||
if from == to {
|
||||
return;
|
||||
}
|
||||
let after = pos.y > rect.center().y;
|
||||
let y = if after { rect.bottom() } else { rect.top() };
|
||||
ui.painter().line_segment([egui::pos2(r.left(), y), egui::pos2(r.right(), y)], Stroke::new(2.0, t.accent));
|
||||
if released && take() {
|
||||
let to = to + usize::from(after);
|
||||
let to = if from < to { to - 1 } else { to };
|
||||
*act = Some(Act::Run("library.moveGroup", json!({ "group": moving, "to": to })));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A section heading in the list.
|
||||
fn heading(ui: &mut Ui, text: &str) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
@@ -584,6 +782,131 @@ mod tests {
|
||||
assert!(none.iter().any(|t| t == "No results"), "{none:?}");
|
||||
}
|
||||
|
||||
/// A library imported from `styles` (character style names) and `groups` ([(name, its styles)])
|
||||
/// → its id.
|
||||
fn styled(app: &mut VectorcraftApp, styles: &[&str], groups: &[(&str, &[&str])]) -> String {
|
||||
let styles: Vec<Value> = styles.iter().enumerate().map(|(i, n)| json!({"name": n, "attrs": {"size": 8 + i}})).collect();
|
||||
let groups: Vec<Value> = groups
|
||||
.iter()
|
||||
.map(|(g, items)| json!({"name": g, "items": items.iter().map(|i| json!({"kind": "charStyle", "item": i})).collect::<Vec<_>>()}))
|
||||
.collect();
|
||||
let data = json!({"name": "Order", "charStyles": styles, "groups": groups}).to_string();
|
||||
app.run("library.import", json!({"data": data})).unwrap()["id"].as_str().unwrap().to_string()
|
||||
}
|
||||
|
||||
/// One frame of the panel in a 260 × `tall` window with `events` → where each text it shows is
|
||||
/// (its rect), and the panel's height.
|
||||
fn frame_at(app: &mut VectorcraftApp, ctx: &egui::Context, tall: f32, events: Vec<egui::Event>) -> (Vec<(String, Rect)>, f32) {
|
||||
let raw = egui::RawInput { screen_rect: Some(Rect::from_min_size(egui::pos2(0.0, 0.0), vec2(260.0, tall))), events, ..Default::default() };
|
||||
let mut height = 0.0;
|
||||
let mut out = ctx.run_ui(raw, |ui| {
|
||||
show(app, ui);
|
||||
height = ui.min_rect().height();
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
let mut texts = vec![];
|
||||
for s in out.shapes {
|
||||
if let egui::epaint::Shape::Text(t) = s.shape {
|
||||
texts.push((t.galley.text().to_string(), t.galley.rect.translate(t.pos.to_vec2())));
|
||||
}
|
||||
}
|
||||
(texts, height)
|
||||
}
|
||||
|
||||
/// Where text `name` is in `texts`.
|
||||
fn at(texts: &[(String, Rect)], name: &str) -> Rect {
|
||||
texts.iter().find(|(t, _)| t == name).map(|(_, r)| *r).unwrap_or_else(|| panic!("{name} isn't shown: {texts:?}"))
|
||||
}
|
||||
|
||||
/// `drag` released at `pos`.
|
||||
fn drop_at(app: &mut VectorcraftApp, ctx: &egui::Context, drag: PanelDrag, pos: egui::Pos2) {
|
||||
egui::DragAndDrop::set_payload(ctx, drag);
|
||||
let up = egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed: false, modifiers: Default::default() };
|
||||
frame_at(app, ctx, 600.0, vec![egui::Event::PointerMoved(pos), up]);
|
||||
frame_at(app, ctx, 600.0, vec![]);
|
||||
}
|
||||
|
||||
fn style_order(app: &mut VectorcraftApp) -> Vec<String> {
|
||||
let got = app.run("library.get", json!({})).unwrap();
|
||||
got["charStyles"].as_array().unwrap().iter().map(|s| s["name"].as_str().unwrap().to_string()).collect()
|
||||
}
|
||||
|
||||
fn group_names(app: &mut VectorcraftApp) -> Vec<(String, Vec<String>)> {
|
||||
let got = app.run("library.get", json!({})).unwrap();
|
||||
let items = |g: &Value| g["items"].as_array().unwrap().iter().map(|i| i["item"].as_str().unwrap().to_string()).collect();
|
||||
got["groups"].as_array().unwrap().iter().map(|g| (g["name"].as_str().unwrap().to_string(), items(g))).collect()
|
||||
}
|
||||
|
||||
/// Dragging in the list: a style dropped on the top or bottom half of another lands before or
|
||||
/// after it, in its group; one dropped on a group's header goes into that group; a group's
|
||||
/// header dropped on another group moves the group; and with every item grouped, a strip
|
||||
/// takes an item out of its group.
|
||||
#[test]
|
||||
fn items_and_groups_move_by_dragging() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 200, "height": 200})).unwrap();
|
||||
let lib = styled(&mut app, &["A", "B", "C", "D"], &[("Warm", &["D"]), ("Cool", &[])]);
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
let (texts, _) = frame_at(&mut app, &ctx, 600.0, vec![]);
|
||||
let item = |name: &str| PanelDrag::LibraryItem { library: lib.clone(), kind: ItemKind::CharStyle, item: name.into() };
|
||||
// C onto A's top half: first.
|
||||
let a = at(&texts, "A");
|
||||
drop_at(&mut app, &ctx, item("C"), egui::pos2(a.center().x, a.top() - 4.0));
|
||||
assert_eq!(style_order(&mut app), ["C", "A", "B", "D"]);
|
||||
// A onto D's bottom half (in Warm): after D, in Warm.
|
||||
let (texts, _) = frame_at(&mut app, &ctx, 600.0, vec![]);
|
||||
let d = at(&texts, "D");
|
||||
drop_at(&mut app, &ctx, item("A"), egui::pos2(d.center().x, d.bottom() + 4.0));
|
||||
assert_eq!(style_order(&mut app), ["C", "B", "D", "A"]);
|
||||
assert_eq!(group_names(&mut app), [("Warm".into(), vec!["D".to_string(), "A".into()]), ("Cool".into(), vec![])]);
|
||||
// B onto Cool's header: into Cool, the order kept.
|
||||
let (texts, _) = frame_at(&mut app, &ctx, 600.0, vec![]);
|
||||
drop_at(&mut app, &ctx, item("B"), at(&texts, "Cool").center());
|
||||
assert_eq!(style_order(&mut app), ["C", "B", "D", "A"]);
|
||||
assert_eq!(group_names(&mut app)[1], ("Cool".into(), vec!["B".to_string()]));
|
||||
// Cool's header onto Warm's header (its top half): Cool first.
|
||||
let (texts, _) = frame_at(&mut app, &ctx, 600.0, vec![]);
|
||||
let warm = at(&texts, "Warm");
|
||||
drop_at(&mut app, &ctx, PanelDrag::LibraryGroup { library: lib.clone(), group: "Cool".into() }, egui::pos2(warm.center().x, warm.top()));
|
||||
assert_eq!(group_names(&mut app).iter().map(|(g, _)| g.as_str()).collect::<Vec<_>>(), ["Cool", "Warm"]);
|
||||
// Every item grouped: while one is dragged a strip takes it out of its group.
|
||||
app.run("library.moveItem", json!({"kind": "charStyle", "item": "C", "group": "Warm"})).unwrap();
|
||||
egui::DragAndDrop::set_payload(&ctx, item("C"));
|
||||
let (texts, _) = frame_at(&mut app, &ctx, 600.0, vec![]);
|
||||
let strip = at(&texts, "Drop here to take it out of its group");
|
||||
drop_at(&mut app, &ctx, item("C"), strip.center());
|
||||
assert!(group_names(&mut app).iter().all(|(_, items)| !items.contains(&"C".to_string())), "{:?}", group_names(&mut app));
|
||||
// Without a drag there is no strip.
|
||||
let (texts, _) = frame_at(&mut app, &ctx, 600.0, vec![]);
|
||||
assert!(!texts.iter().any(|(t, _)| t == "Drop here to take it out of its group"));
|
||||
}
|
||||
|
||||
/// A long list scrolls inside the panel (its bottom bar stays in the window), and a drag held
|
||||
/// near the list's bottom edge scrolls it.
|
||||
#[test]
|
||||
fn a_long_list_scrolls_also_while_dragging() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 200, "height": 200})).unwrap();
|
||||
let names: Vec<String> = (0..60).map(|i| format!("Style {i}")).collect();
|
||||
let lib = styled(&mut app, &names.iter().map(String::as_str).collect::<Vec<_>>(), &[]);
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
let (texts, height) = frame_at(&mut app, &ctx, 400.0, vec![]);
|
||||
assert!(height <= 400.0, "the panel fits the window: {height}");
|
||||
let first = at(&texts, "Style 0");
|
||||
// The list's bottom edge: the last text drawn before the window's bottom bar area.
|
||||
let bottom = texts.iter().filter(|(t, _)| t.starts_with("Style ")).map(|(_, r)| r.bottom()).fold(0.0, f32::max).min(400.0 - BAR - 4.0);
|
||||
egui::DragAndDrop::set_payload(&ctx, PanelDrag::LibraryItem { library: lib, kind: ItemKind::CharStyle, item: "Style 0".into() });
|
||||
let hold = egui::pos2(first.center().x, bottom - 4.0);
|
||||
for _ in 0..5 {
|
||||
frame_at(&mut app, &ctx, 400.0, vec![egui::Event::PointerMoved(hold)]);
|
||||
}
|
||||
let (texts, _) = frame_at(&mut app, &ctx, 400.0, vec![egui::Event::PointerMoved(hold)]);
|
||||
let moved = texts.iter().find(|(t, _)| t == "Style 0").map_or(f32::NEG_INFINITY, |(_, r)| r.top());
|
||||
assert!(moved < first.top(), "the list scrolled down: {} → {moved}", first.top());
|
||||
}
|
||||
|
||||
/// New Group… and Rename Group… ask for a name and act on the library and group they came
|
||||
/// from; the dialog shows only the name.
|
||||
#[test]
|
||||
|
||||
@@ -410,7 +410,7 @@ fn drag_of(e: &Entry, names: Vec<String>) -> PanelDrag {
|
||||
Entry::Folder(_) => Paint::None,
|
||||
};
|
||||
let rows = SwatchRows { grabbed: e.name().to_string(), names, groups: e.is_folder() };
|
||||
PanelDrag::Paint { paint, params: swatch_params(e).unwrap_or_default(), rows: Some(rows) }
|
||||
PanelDrag::Paint { paint, params: swatch_params(e).unwrap_or_default(), rows: Some(rows), library: None }
|
||||
}
|
||||
|
||||
/// What the tiles or rows saw this frame.
|
||||
@@ -614,6 +614,7 @@ pub(crate) fn drag_preview(app: &VectorcraftApp, ctx: &egui::Context) {
|
||||
},
|
||||
PanelDrag::Brush { def, .. } => return pointer_chip(ctx, |ui, r| super::brushes::chip(ui, r, def)),
|
||||
PanelDrag::LibraryGraphic { library, item } => return pointer_chip(ctx, |ui, r| super::libraries::chip(app, ui, r, library, item)),
|
||||
PanelDrag::LibraryItem { .. } | PanelDrag::LibraryGroup { .. } => return,
|
||||
};
|
||||
pointer_chip(ctx, |ui, r| {
|
||||
if registration {
|
||||
|
||||
@@ -2034,8 +2034,9 @@ pub enum PanelDrag {
|
||||
/// is dropped on takes `params` (`{swatch}`, `{color}`, `{gradient}`…; null for a colour group,
|
||||
/// which paints nothing) through the active proxy's `paint.setFill`/`paint.setStroke`; the
|
||||
/// Gradient panel's ramp takes its colour as a stop; the Swatches panel moves `rows`, or makes
|
||||
/// a swatch of a paint dragged from elsewhere.
|
||||
Paint { paint: Paint, params: serde_json::Value, rows: Option<SwatchRows> },
|
||||
/// a swatch of a paint dragged from elsewhere. `library`: the Libraries panel colour it is
|
||||
/// (library id, colour name), which that panel moves to where it is dropped.
|
||||
Paint { paint: Paint, params: serde_json::Value, rows: Option<SwatchRows>, library: Option<(String, String)> },
|
||||
/// The Appearance panel's thumbnail: art it is dropped on takes object `0`'s appearance
|
||||
/// (`appearance.copyFrom`); the Graphic Styles panel makes a style of it.
|
||||
Appearance(vectorcraft_doc::NodeId),
|
||||
@@ -2054,8 +2055,13 @@ pub enum PanelDrag {
|
||||
/// dropped on takes it (`brush.apply`).
|
||||
Brush { name: String, def: serde_json::Value },
|
||||
/// A Libraries panel graphic (`item`: its id in `library`): the canvas places a copy centred
|
||||
/// where it is dropped (`library.use`).
|
||||
/// where it is dropped (`library.use`); the panel moves it to where it is dropped.
|
||||
LibraryGraphic { library: String, item: String },
|
||||
/// A Libraries panel text style (`item`: its name in `library`), which the panel moves to where
|
||||
/// it is dropped.
|
||||
LibraryItem { library: String, kind: vectorcraft_engine::cmd::library::ItemKind, item: String },
|
||||
/// A Libraries panel group, by its header, which the panel moves to where it is dropped.
|
||||
LibraryGroup { library: String, group: String },
|
||||
}
|
||||
|
||||
/// A panel list `zone` that takes art dragged off the canvas: outlined while art is held over it;
|
||||
@@ -2088,7 +2094,7 @@ impl PanelDrag {
|
||||
g.geom = None;
|
||||
g.freeform = None;
|
||||
}
|
||||
Self::Paint { params: crate::panels::paint_params(&paint), paint, rows: None }
|
||||
Self::Paint { params: crate::panels::paint_params(&paint), paint, rows: None, library: None }
|
||||
}
|
||||
/// The dragged colour (a solid paint's), which the Gradient panel's ramp takes.
|
||||
pub fn color(&self) -> Option<vectorcraft_color::Color> {
|
||||
|
||||
+7
-3
@@ -1564,9 +1564,13 @@ Items can be put in user-named groups, which the panel shows as collapsible sect
|
||||
search field filters every item by name; `library.get` lists them all to filter). `library.createGroup {library?,
|
||||
name?, items?: [{kind, item}]}` makes one (a name another group has, ignoring case, gets a number) with those items
|
||||
moved into it; `library.renameGroup {library?, group, name}` renames one; `library.deleteGroup {library?, group}`
|
||||
deletes one, its items staying in the library ungrouped; and `library.moveItem {library?, kind, item, group?}` moves
|
||||
an item into a group (without `group`, out of its group). An item is in one group at most; `library.get` lists the
|
||||
groups as `groups: [{name, items: [{kind: "color"|"charStyle"|"paraStyle"|"graphic", item}]}]` (a graphic by its id).
|
||||
deletes one, its items staying in the library ungrouped; and `library.moveItem {library?, kind, item, group?, before?
|
||||
| after?}` moves an item into a group (without `group`, out of its group) and, with `before` or `after` (another item
|
||||
of its kind), just before or after that item in the order its kind is listed and shown in. `library.moveGroup
|
||||
{library?, group, to}` moves a group to place `to` of the list (0: first; past the end: last). Dragging items and
|
||||
group headers in the panel runs these. An item is in one group at most; `library.get` lists the groups as `groups:
|
||||
[{name, items: [{kind: "color"|"charStyle"|"paraStyle"|"graphic", item}]}]` (a graphic by its id), and each kind's
|
||||
items in the order the panel shows them.
|
||||
|
||||
`library.export {library?, path?}` writes a library (its colours, styles, graphics with their art and thumbnails, and
|
||||
its groups) as a `.vclibrary` file, the JSON the library folder keeps (without `path`: `{data}`, the file's text).
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user