mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-29 12:17:14 -06:00
Merge pull request #3162 from danngreen/feat/libraries-panel-scroll-reorder
Libraries panel: scrolling, and drag to reorder items and groups
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