mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-28 03:37:00 -06:00
refactor(ui-egui): move panels/swatches.rs items to panels/swatches/{actions,body,dnd,libraries,tiles}.rs
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,199 @@
|
||||
use super::dnd::{group_of, selection};
|
||||
use super::libraries::SwatchLibraries;
|
||||
use super::*;
|
||||
|
||||
/// The selected swatches and groups that can be deleted (all but None and Registration).
|
||||
pub(super) fn deletable(sel: &[String]) -> Vec<String> {
|
||||
sel.iter().filter(|n| !n.starts_with('[')).cloned().collect()
|
||||
}
|
||||
|
||||
/// The selected swatch when exactly one swatch with an editor is selected (not None, Registration
|
||||
/// or a group).
|
||||
fn editable(app: &VectorcraftApp, sel: &[String]) -> Option<String> {
|
||||
match sel {
|
||||
[n] if !n.starts_with('[') && app.session.active().is_some_and(|st| st.doc.swatch(n).is_some()) => Some(n.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The selected solid-colour swatches in selection order (Merge Swatches keeps the first).
|
||||
pub(super) fn mergeable(app: &VectorcraftApp, sel: &[String]) -> Vec<String> {
|
||||
let Some(st) = app.session.active() else { return vec![] };
|
||||
sel.iter().filter(|n| st.doc.swatch(n).is_some_and(|w| w.paint.color().is_some())).cloned().collect()
|
||||
}
|
||||
|
||||
/// The first selected colour group (Ungroup Color Group).
|
||||
pub(super) fn selected_group(app: &VectorcraftApp, sel: &[String]) -> Option<String> {
|
||||
let d = &app.session.active()?.doc;
|
||||
sel.iter().find(|n| d.swatch_groups.iter().any(|g| g.name == **n)).cloned()
|
||||
}
|
||||
|
||||
/// Edit or Apply Color Group: Recolor Artwork on colour group `group` (and the selected art).
|
||||
pub(super) fn edit_group(app: &mut VectorcraftApp, group: &str) {
|
||||
app.run("ui.recolorDialog", json!({ "group": group })).ok();
|
||||
}
|
||||
|
||||
/// Delete `names` (swatches and colour groups) after asking, or at once with `now` (Alt-click).
|
||||
pub(super) fn delete(app: &mut VectorcraftApp, names: Vec<String>, now: bool) {
|
||||
let params = json!({"names": names});
|
||||
if now {
|
||||
app.run("swatch.delete", params).ok();
|
||||
return;
|
||||
}
|
||||
let message = match names.as_slice() {
|
||||
[n] => crate::i18n::fmt(tl!("Delete “{name}”?"), &[("name", n)]),
|
||||
_ => crate::i18n::fmt(tl!("Delete these {count} swatches and groups?"), &[("count", &names.len().to_string())]),
|
||||
};
|
||||
crate::dialogs::confirm::ask(app, &message, tl!("Art using a deleted global swatch keeps its colour."), "swatch.delete", params);
|
||||
}
|
||||
|
||||
pub(super) fn bottom(app: &mut VectorcraftApp, ui: &mut Ui, sel: &[String]) {
|
||||
let kind: Kind = pstate(ui.ctx(), "swatch-kind");
|
||||
widgets::bottom_bar(ui, |ui| {
|
||||
let open = library_panel::any_open::<SwatchLibraries>(&app.ui);
|
||||
let lr = widgets::icon_button(ui, "library", SwatchLibraries::MENU, open, 24.0);
|
||||
egui::Popup::menu(&lr).show(|ui| {
|
||||
ui.set_min_width(200.0);
|
||||
library_panel::library_menu::<SwatchLibraries>(app, ui);
|
||||
});
|
||||
let kr = widgets::icon_button(ui, "dc-swatch-kinds", tl!("Show Swatch Kinds"), kind != Kind::All, 24.0);
|
||||
egui::Popup::menu(&kr).show(|ui| {
|
||||
for (k, label) in Kind::ALL {
|
||||
if menu_item(ui, label, true, k == kind) {
|
||||
set_pstate(ui.ctx(), "swatch-kind", k);
|
||||
}
|
||||
}
|
||||
});
|
||||
// With a colour group selected the options button edits or applies the group.
|
||||
let opts = editable(app, sel);
|
||||
if let Some(g) = selected_group(app, sel).filter(|_| opts.is_none()) {
|
||||
if widgets::icon_button(ui, "palette", tl!("Edit or Apply Color Group"), false, 24.0).clicked() {
|
||||
edit_group(app, &g);
|
||||
}
|
||||
} else if widgets::icon_button_enabled(ui, "dc-options", tl!("Swatch Options"), false, opts.is_some(), 24.0).clicked()
|
||||
&& let Some(n) = opts
|
||||
{
|
||||
app.run("ui.swatchOptions", json!({"name": n})).ok();
|
||||
}
|
||||
ui.add_space((ui.available_width() - 3.0 * 28.0).max(0.0));
|
||||
if widgets::icon_button(ui, "dc-folder", tl!("New Color Group"), false, 24.0).clicked() {
|
||||
app.run("ui.newColorGroup", json!({"swatches": deletable(sel)})).ok();
|
||||
}
|
||||
// Alt-click skips the dialog; Ctrl/Cmd-click makes a spot color, and Ctrl/Cmd+Shift-click
|
||||
// makes a global process color.
|
||||
if widgets::icon_button(ui, "dc-new-item", tl!("New Swatch"), false, 24.0).clicked() {
|
||||
new_swatch(app, sel, ui.input(|i| i.modifiers));
|
||||
}
|
||||
// Alt-click deletes without asking.
|
||||
let del = deletable(sel);
|
||||
if widgets::icon_button_enabled(ui, "trash-2", tl!("Delete Swatch"), false, !del.is_empty(), 24.0).clicked() {
|
||||
delete(app, del, ui.input(|i| i.modifiers.alt));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// The colour group the selection points at: the last selected group, or the group of the last
|
||||
/// selected swatch.
|
||||
fn target_group(app: &VectorcraftApp, sel: &[String]) -> Option<String> {
|
||||
group_of(&app.session.active()?.doc, sel.last()?)
|
||||
}
|
||||
|
||||
/// New Swatch from the active paint (a color goes into the selected color group), for a click
|
||||
/// with modifiers `m`: a plain click opens the dialog and an Alt-click saves at once. Cmd/Ctrl
|
||||
/// makes a color a spot color, and Cmd/Ctrl+Shift makes it a global process color.
|
||||
pub(super) fn new_swatch(app: &mut VectorcraftApp, sel: &[String], m: egui::Modifiers) {
|
||||
let paint = active_paint(app);
|
||||
let group = paint.color().and_then(|_| target_group(app, sel));
|
||||
let (spot, global) = (m.command && !m.shift, m.command && m.shift);
|
||||
if !m.alt {
|
||||
app.run("ui.newSwatch", json!({"spot": spot, "global": global, "group": group})).ok();
|
||||
return;
|
||||
}
|
||||
let mut params = crate::panels::swatch_paint_params(&paint);
|
||||
if paint.color().is_some() {
|
||||
params["spot"] = json!(spot);
|
||||
params["global"] = json!(global);
|
||||
}
|
||||
if let Some(g) = group {
|
||||
params["group"] = json!(g);
|
||||
}
|
||||
app.run("swatch.new", params).ok();
|
||||
}
|
||||
|
||||
pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let view: View = pstate(ui.ctx(), "swatch-view");
|
||||
let selected = selection(app, ui);
|
||||
let del = deletable(&selected);
|
||||
let opts = editable(app, &selected);
|
||||
if menu_item(ui, tl!("New Swatch…"), true, false) {
|
||||
new_swatch(app, &selected, egui::Modifiers::NONE);
|
||||
}
|
||||
if menu_item(ui, tl!("New Color Group…"), true, false) {
|
||||
app.run("ui.newColorGroup", json!({"swatches": del})).ok();
|
||||
}
|
||||
if menu_item(ui, tl!("Duplicate Swatch"), opts.is_some(), false)
|
||||
&& let Some(n) = &opts
|
||||
{
|
||||
app.run("swatch.duplicate", json!({"name": n})).ok();
|
||||
}
|
||||
// The first selected colour is kept.
|
||||
let merge = mergeable(app, &selected);
|
||||
if menu_item(ui, tl!("Merge Swatches"), merge.len() > 1, false) {
|
||||
app.run("swatch.merge", json!({ "names": merge })).ok();
|
||||
}
|
||||
if menu_item(ui, tl!("Delete Swatch"), !del.is_empty(), false) {
|
||||
delete(app, del, false);
|
||||
}
|
||||
let group = selected_group(app, &selected);
|
||||
if menu_item(ui, tl!("Edit or Apply Color Group…"), group.is_some(), false)
|
||||
&& let Some(g) = &group
|
||||
{
|
||||
edit_group(app, g);
|
||||
}
|
||||
if menu_item(ui, tl!("Ungroup Color Group"), group.is_some(), false)
|
||||
&& let Some(g) = group
|
||||
{
|
||||
app.run("swatch.ungroup", json!({ "name": g })).ok();
|
||||
}
|
||||
if menu_item(ui, tl!("Select All Unused"), true, false)
|
||||
&& let Ok(r) = app.run("swatch.unused", json!({}))
|
||||
{
|
||||
let names: Vec<String> = r["names"].as_array().into_iter().flatten().filter_map(Value::as_str).map(str::to_string).collect();
|
||||
set_pstate(ui.ctx(), "swatch-selected", names);
|
||||
}
|
||||
if menu_item(ui, tl!("Add Used Colors"), true, false) {
|
||||
app.run("swatch.addUsedColors", json!({})).ok();
|
||||
}
|
||||
ui.separator();
|
||||
if menu_item(ui, tl!("Sort by Name"), true, false) {
|
||||
app.run("swatch.sortByName", json!({})).ok();
|
||||
}
|
||||
if menu_item(ui, tl!("Sort by Kind"), true, false) {
|
||||
app.run("swatch.sortByKind", json!({})).ok();
|
||||
}
|
||||
ui.separator();
|
||||
for (v, label) in View::ALL {
|
||||
if menu_item(ui, label, true, v == view) {
|
||||
set_pstate(ui.ctx(), "swatch-view", v);
|
||||
}
|
||||
}
|
||||
ui.separator();
|
||||
let find: bool = pstate(ui.ctx(), "swatch-show-find");
|
||||
if menu_item(ui, tl!("Show Find Field"), true, find) {
|
||||
set_pstate(ui.ctx(), "swatch-show-find", !find);
|
||||
}
|
||||
ui.separator();
|
||||
if menu_item(ui, tl!("Swatch Options…"), opts.is_some(), false)
|
||||
&& let Some(n) = opts
|
||||
{
|
||||
app.run("ui.swatchOptions", json!({"name": n})).ok();
|
||||
}
|
||||
if menu_item(ui, tl!("Spot Colors…"), app.session.active().is_some(), false) {
|
||||
app.run("ui.spotColors", json!({})).ok();
|
||||
}
|
||||
ui.separator();
|
||||
ui.menu_button(tl!("Open Swatch Library"), |ui| library_panel::library_menu::<SwatchLibraries>(app, ui));
|
||||
if menu_item(ui, tl!("Save Swatch Library…"), true, false) {
|
||||
app.run("ui.saveSwatchLibrary", json!({ "names": deletable(&selected) })).ok();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
use super::actions::{bottom, edit_group};
|
||||
use super::dnd::{TileEvents, apply_drop, click_selection, highlighted, selection, tile_input, zone_input};
|
||||
use super::tiles::{
|
||||
draw_folder, draw_registration, folder_tile, global_mark, list_icons, list_row, list_row_name, pattern_thumb, tile_grid, tile_id,
|
||||
};
|
||||
use super::*;
|
||||
|
||||
/// The id of the find field's text (shown with the panel menu's Show Find Field).
|
||||
pub(super) fn find_id() -> egui::Id {
|
||||
egui::Id::unique("swatch-find")
|
||||
}
|
||||
|
||||
/// The panel's body; `salt` keys its tiles (see [`tile_id`]).
|
||||
pub(super) fn body(app: &mut VectorcraftApp, ui: &mut Ui, salt: &'static str) {
|
||||
if app.session.active().is_none() {
|
||||
crate::panels::empty_state(ui, "swatch-book", tl!("No document"), tl!("Open a document to see its swatches."));
|
||||
return;
|
||||
}
|
||||
let view: View = pstate(ui.ctx(), "swatch-view");
|
||||
let kind: Kind = pstate(ui.ctx(), "swatch-kind");
|
||||
// Top row: proxy at left, list / grid view toggles at right.
|
||||
ui.horizontal(|ui| {
|
||||
crate::panels::proxy(app, ui, 34.0);
|
||||
ui.add_space(ui.available_width() - 58.0);
|
||||
if widgets::icon_button(ui, "dc-list-view", tl!("Show List View"), view.is_list(), 26.0).clicked() {
|
||||
set_pstate(ui.ctx(), "swatch-view", View::SmallList);
|
||||
}
|
||||
if widgets::icon_button(ui, "dc-grid-view", tl!("Show Thumbnail View"), !view.is_list(), 26.0).clicked() {
|
||||
set_pstate(ui.ctx(), "swatch-view", View::MediumThumb);
|
||||
}
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
// A recent colour paints the active proxy (Alt: the inactive one), as a swatch does.
|
||||
if let Some(c) = crate::panels::recent_colors_row(app, ui) {
|
||||
crate::panels::apply_click(app, ui, json!({"color": crate::panels::color_json(&c)}));
|
||||
}
|
||||
widgets::divider(ui);
|
||||
widgets::subheader(ui, tl!("Swatch Tiles"));
|
||||
let query = if pstate(ui.ctx(), "swatch-show-find") { widgets::search_field(ui, find_id(), tl!("Find")) } else { String::new() };
|
||||
let items = entries(app, kind, &query);
|
||||
let active_swatch = kept_swatch(app, ui, active_swatch(app));
|
||||
let focus = focus_swatch(app, ui, active_swatch.as_deref());
|
||||
let selected = selection(app, ui);
|
||||
// Without a selection the swatch of the active paint is highlighted.
|
||||
let fallback: Vec<String> = if selected.is_empty() { active_swatch.into_iter().collect() } else { vec![] };
|
||||
let sel = if selected.is_empty() { &fallback } else { &selected };
|
||||
let lit = app.session.active().map(|st| highlighted(&st.doc, sel)).unwrap_or_default();
|
||||
let is_sel = |name: &str| lit.contains(&name);
|
||||
let mut ev = TileEvents::default();
|
||||
widgets::list_box(ui, |ui| {
|
||||
let max_height = if view == View::LargeThumb { 200.0 } else { 150.0 };
|
||||
let out = egui::ScrollArea::vertical().id_salt("swatch-scroll").max_height(max_height).show(ui, |ui| {
|
||||
ui.set_width(ui.available_width());
|
||||
if view.is_list() {
|
||||
for e in &items {
|
||||
let name = e.name();
|
||||
let (r, resp, chip) = list_row(ui, tile_id(salt, e), view, is_sel(name));
|
||||
if focus.as_deref() == Some(name) {
|
||||
ui.scroll_to_rect(r, Some(egui::Align::Center));
|
||||
}
|
||||
match e {
|
||||
Entry::Registration => draw_registration(ui, chip),
|
||||
Entry::Swatch { paint, global, spot, .. } => {
|
||||
swatch_tile(ui, chip, paint, false, false);
|
||||
pattern_thumb(app, ui, chip, paint);
|
||||
list_icons(ui, r, paint, *global, *spot);
|
||||
}
|
||||
Entry::Folder(_) => draw_folder(ui, chip),
|
||||
}
|
||||
list_row_name(ui, r, chip, crate::i18n::t_name("swatch", name));
|
||||
tile_input(ui, resp, e, &items, sel, true, &mut ev);
|
||||
}
|
||||
} else {
|
||||
for (e, cell) in tile_grid(ui, &items, view, Entry::is_folder) {
|
||||
let resp = ui.interact(cell, tile_id(salt, e), Sense::click_and_drag());
|
||||
let name = e.name();
|
||||
if focus.as_deref() == Some(name) {
|
||||
ui.scroll_to_rect(cell, Some(egui::Align::Center));
|
||||
}
|
||||
match e {
|
||||
Entry::Registration => draw_registration(ui, cell),
|
||||
Entry::Swatch { paint, global, spot, .. } => {
|
||||
swatch_tile(ui, cell, paint, is_sel(name), resp.hovered());
|
||||
pattern_thumb(app, ui, cell.shrink(1.0), paint);
|
||||
if *global {
|
||||
global_mark(ui, cell, *spot);
|
||||
}
|
||||
}
|
||||
Entry::Folder(_) => folder_tile(ui, cell, is_sel(name)),
|
||||
}
|
||||
tile_input(ui, resp, e, &items, sel, false, &mut ev);
|
||||
}
|
||||
}
|
||||
});
|
||||
let zone = ui.interact(out.inner_rect, ui.scope_id().with("swatch-drop"), Sense::hover());
|
||||
zone_input(ui, &zone, &mut ev);
|
||||
});
|
||||
match ev.edit {
|
||||
Some(Entry::Folder(g)) => edit_group(app, &g),
|
||||
Some(e) => {
|
||||
app.run("ui.swatchOptions", json!({"name": e.name()})).ok();
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
if let Some(drop) = ev.drop {
|
||||
apply_drop(app, drop);
|
||||
}
|
||||
let selected = match ev.clicked {
|
||||
Some((e, m)) => {
|
||||
let order: Vec<&str> = items.iter().map(Entry::name).collect();
|
||||
let sel = click_selection(ui, "swatch-anchor", selected, &order, e.name(), m);
|
||||
set_pstate(ui.ctx(), "swatch-selected", sel.clone());
|
||||
// A plain click also applies the swatch; modifier clicks only select.
|
||||
if !(m.shift || m.command) {
|
||||
apply(app, ui, &e);
|
||||
}
|
||||
sel
|
||||
}
|
||||
None => selected,
|
||||
};
|
||||
bottom(app, ui, &selected);
|
||||
}
|
||||
|
||||
/// The document and the objects selected in it.
|
||||
type SelectedArt = (u64, Vec<u64>);
|
||||
|
||||
/// The swatch highlighted for the active paint (`active`): with art selected, its swatch; once the
|
||||
/// art is deselected, still that swatch, until another document shows or the paints for new art
|
||||
/// change (a swatch or colour picked with nothing selected). Deselecting doesn't jump to the
|
||||
/// swatch of the paint new art gets (White).
|
||||
pub(super) fn kept_swatch(app: &VectorcraftApp, ui: &Ui, active: Option<String>) -> Option<String> {
|
||||
let Some(st) = app.session.active() else { return active };
|
||||
let p = &app.session.paint;
|
||||
// The document, and the paints new art gets, which picking with nothing selected changes.
|
||||
let defaults = format!("{}|{:?}|{:?}|{}", st.uid, p.fill, p.stroke, app.session.fill_active);
|
||||
if !st.selection.objects.is_empty() {
|
||||
set_pstate(ui.ctx(), "swatch-kept", Some((defaults, active.clone())));
|
||||
return active;
|
||||
}
|
||||
match pstate::<Option<(String, Option<String>)>>(ui.ctx(), "swatch-kept") {
|
||||
Some((d, kept)) if d == defaults => kept,
|
||||
Some(_) => {
|
||||
set_pstate(ui.ctx(), "swatch-kept", None::<(String, Option<String>)>);
|
||||
active
|
||||
}
|
||||
None => active,
|
||||
}
|
||||
}
|
||||
|
||||
/// The swatch the tiles scroll to this frame, once: the active paint's swatch when it changed, or
|
||||
/// when other art was selected. Selecting other art also clears the panel's own selection, so its
|
||||
/// swatch is the one highlighted.
|
||||
pub(super) fn focus_swatch(app: &VectorcraftApp, ui: &Ui, active: Option<&str>) -> Option<String> {
|
||||
let art: Option<SelectedArt> = app.session.active().map(|st| (st.uid, st.selection.objects.iter().map(|id| id.0).collect()));
|
||||
// (None the first time the panel shows.)
|
||||
let (last_art, last_swatch): (Option<Option<SelectedArt>>, Option<String>) = pstate(ui.ctx(), "swatch-focus");
|
||||
let reselected = last_art.as_ref().is_some_and(|l| *l != art);
|
||||
if reselected {
|
||||
set_pstate(ui.ctx(), "swatch-selected", Vec::<String>::new());
|
||||
}
|
||||
let changed = reselected || last_art.is_none() || last_swatch.as_deref() != active;
|
||||
set_pstate(ui.ctx(), "swatch-focus", (Some(art), active.map(str::to_string)));
|
||||
active.filter(|_| changed).map(str::to_string)
|
||||
}
|
||||
|
||||
/// The swatch the active paint comes from: a linked colour's global swatch (a tint's tint swatch
|
||||
/// when there is one), a process colour's swatch of that colour (a colour no swatch has: none, so
|
||||
/// text in a colour of its own highlights nothing), a gradient's gradient swatch, or None; nothing
|
||||
/// for selected images.
|
||||
pub(super) fn active_swatch(app: &VectorcraftApp) -> Option<String> {
|
||||
// Images and placed files have no fill or stroke: no swatch (not None).
|
||||
let st = app.session.active()?;
|
||||
let unpainted = |id: &vectorcraft_doc::NodeId| {
|
||||
st.doc.node(*id).is_some_and(|n| matches!(n.kind, vectorcraft_doc::NodeKind::Image(_) | vectorcraft_doc::NodeKind::PlacedDocument(_)))
|
||||
};
|
||||
if !st.selection.objects.is_empty() && st.selection.objects.iter().all(unpainted) {
|
||||
return None;
|
||||
}
|
||||
match active_paint(app) {
|
||||
Paint::Solid { swatch: Some(n), tint, .. } => {
|
||||
let d = &app.session.active()?.doc;
|
||||
let tint_swatch = d.swatches_iter().find(|w| w.tint_of() == Some((n.as_str(), tint)));
|
||||
Some(tint_swatch.map_or(n.clone(), |w| w.name.clone()))
|
||||
}
|
||||
// A process colour isn't linked: the swatch of that very colour.
|
||||
Paint::Solid { color, swatch: None, .. } => {
|
||||
let d = &app.session.active()?.doc;
|
||||
d.swatches_iter().find(|w| matches!(w.paint, Paint::Solid { color: c, swatch: None, .. } if c == color)).map(|w| w.name.clone())
|
||||
}
|
||||
Paint::Gradient(g) => g.swatch,
|
||||
Paint::None => Some("[None]".to_string()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
use super::tiles::{describe, draw_registration, pattern_thumb};
|
||||
use super::*;
|
||||
|
||||
/// The swatches and colour groups selected in the panel, in click order, without names that no
|
||||
/// longer exist (deleted, renamed or undone).
|
||||
pub(super) fn selection(app: &VectorcraftApp, ui: &Ui) -> Vec<String> {
|
||||
let Some(st) = app.session.active() else { return vec![] };
|
||||
let mut names: Vec<String> = pstate(ui.ctx(), "swatch-selected");
|
||||
names.retain(|n| n == REGISTRATION || st.doc.swatch_name_taken(n));
|
||||
names
|
||||
}
|
||||
|
||||
/// The names the panel highlights: `sel` plus the swatches of selected colour groups (clicking a
|
||||
/// folder selects the whole group).
|
||||
pub(super) fn highlighted<'a>(d: &'a Document, sel: &'a [String]) -> Vec<&'a str> {
|
||||
let mut out: Vec<&str> = sel.iter().map(String::as_str).collect();
|
||||
for g in d.swatch_groups.iter().filter(|g| sel.contains(&g.name)) {
|
||||
out.extend(g.swatches.iter().map(|w| w.name.as_str()));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The selection after a click on `name`: Cmd/Ctrl toggles it, Shift extends from the last clicked
|
||||
/// name (kept in panel state `anchor_key`) over `order` (the names as displayed), a plain click
|
||||
/// selects it alone.
|
||||
pub(crate) fn click_selection(ui: &Ui, anchor_key: &str, mut sel: Vec<String>, order: &[&str], name: &str, m: egui::Modifiers) -> Vec<String> {
|
||||
let anchor: String = pstate(ui.ctx(), anchor_key);
|
||||
let pos = |n: &str| order.iter().position(|o| *o == n);
|
||||
if m.shift
|
||||
&& let (Some(a), Some(b)) = (pos(&anchor), pos(name))
|
||||
{
|
||||
for n in &order[a.min(b)..=a.max(b)] {
|
||||
if !sel.iter().any(|s| s == n) {
|
||||
sel.push(n.to_string());
|
||||
}
|
||||
}
|
||||
return sel;
|
||||
}
|
||||
set_pstate(ui.ctx(), anchor_key, name.to_string());
|
||||
if !m.command {
|
||||
return vec![name.to_string()];
|
||||
}
|
||||
match sel.iter().position(|s| s == name) {
|
||||
Some(i) => {
|
||||
sel.remove(i);
|
||||
}
|
||||
None => sel.push(name.to_string()),
|
||||
}
|
||||
sel
|
||||
}
|
||||
|
||||
/// A drag released on the panel.
|
||||
pub(super) enum Drop {
|
||||
/// Swatches (or colour groups) dropped before or `after` row `target`, into folder `target`, or
|
||||
/// at the end (`None`). With Alt held, `with` names the swatch grabbed: dropped on swatch
|
||||
/// `target` of its kind (neither None, Registration nor a tint swatch; see [`replaces`]), it
|
||||
/// gives that swatch its paint instead of moving.
|
||||
Move { names: Vec<String>, target: Option<String>, after: bool, with: Option<String> },
|
||||
/// A paint from elsewhere (a Fill/Stroke proxy, the Gradient panel's thumbnail) dropped on row
|
||||
/// `target` (or between rows) becomes a swatch; with Alt held (`replace`) it replaces swatch
|
||||
/// `target` when that is of its kind and not a tint swatch.
|
||||
New { paint: Paint, target: Option<String>, replace: bool },
|
||||
/// Art dragged off the canvas becomes a pattern swatch made of a copy of it (the art stays).
|
||||
Pattern { ids: Vec<u64> },
|
||||
}
|
||||
|
||||
impl Drop {
|
||||
/// What releasing `d` before or `after` row `target` (`None`: at the end) does: move the rows
|
||||
/// it carries (with Alt held, `replace`, a swatch grabbed may replace `target`), make a swatch of
|
||||
/// a paint dragged from elsewhere, or a pattern swatch of art dragged off the canvas (appearances:
|
||||
/// nothing).
|
||||
fn of(d: &PanelDrag, target: Option<String>, after: bool, replace: bool) -> Option<Self> {
|
||||
match d {
|
||||
PanelDrag::Paint { rows: Some(r), .. } => {
|
||||
let with = (replace && !r.groups).then(|| r.grabbed.clone());
|
||||
Some(Drop::Move { names: r.names.clone(), target, after, with })
|
||||
}
|
||||
PanelDrag::Paint { paint, .. } => Some(Drop::New { paint: paint.clone(), target, replace }),
|
||||
PanelDrag::Art(ids) => Some(Drop::Pattern { ids: ids.iter().map(|id| id.0).collect() }),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The drag held over `resp`, if the panel takes it (paints and art dragged off the canvas, not the
|
||||
/// Appearance panel's thumbnail).
|
||||
fn held(resp: &Response) -> Option<std::sync::Arc<PanelDrag>> {
|
||||
resp.dnd_hover_payload::<PanelDrag>().filter(|d| matches!(**d, PanelDrag::Paint { .. } | PanelDrag::Art(_)))
|
||||
}
|
||||
|
||||
/// The drag a tile or row of `e` starts, moving rows `names`. Colour groups have no paint of their
|
||||
/// own (the chip under the pointer draws Registration's mark for Registration).
|
||||
pub(super) fn drag_of(e: &Entry, names: Vec<String>) -> PanelDrag {
|
||||
let paint = match e {
|
||||
Entry::Swatch { paint, .. } => paint.clone(),
|
||||
Entry::Registration => Paint::registration(),
|
||||
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), library: None }
|
||||
}
|
||||
|
||||
/// What the tiles or rows saw this frame.
|
||||
#[derive(Default)]
|
||||
pub(super) struct TileEvents {
|
||||
pub(super) clicked: Option<(Entry, egui::Modifiers)>,
|
||||
/// Double-click opens the swatch's editor (Swatch Options, Gradient panel or pattern editing),
|
||||
/// or Edit or Apply Color Group on a colour group.
|
||||
pub(super) edit: Option<Entry>,
|
||||
pub(super) drop: Option<Drop>,
|
||||
/// A drag is held over a tile or row.
|
||||
over: bool,
|
||||
}
|
||||
|
||||
/// Clicks, double-clicks, tooltips and drag and drop of the tile or row `resp` of entry `e`
|
||||
/// (`items`: the panel's rows, `sel`: its selection, `list`: rows stack downwards). A drag from it
|
||||
/// starts a [`PanelDrag`] carrying the rows it moves; see [`tile_drop`] for drags held over it.
|
||||
pub(super) fn tile_input(ui: &Ui, resp: Response, e: &Entry, items: &[Entry], sel: &[String], list: bool, ev: &mut TileEvents) {
|
||||
let name = e.name();
|
||||
if matches!(e, Entry::Swatch { .. } | Entry::Folder(_)) && resp.double_clicked() {
|
||||
ev.edit = Some(e.clone());
|
||||
}
|
||||
if resp.drag_started() {
|
||||
let with_sel = sel.iter().any(|s| s == name);
|
||||
let names = items
|
||||
.iter()
|
||||
.filter(|x| x.is_folder() == e.is_folder() && (x.name() == name || (with_sel && sel.iter().any(|s| s == x.name()))))
|
||||
.map(|x| x.name().to_string())
|
||||
.collect();
|
||||
egui::DragAndDrop::set_payload(ui.ctx(), drag_of(e, names));
|
||||
}
|
||||
tile_drop(ui, &resp, e, list, ev);
|
||||
// The default swatches' names show in the UI's language; the document keeps them as they are.
|
||||
let shown = crate::i18n::t_name("swatch", name);
|
||||
let resp = match e {
|
||||
Entry::Folder(_) => resp.on_hover_text(crate::i18n::fmt(tl!("Color Group: {name}"), &[("name", shown)])),
|
||||
Entry::Swatch { paint, global, spot, .. } if list => resp.on_hover_ui(|ui| {
|
||||
ui.label(format!("{shown} ({})", describe(paint, *global, *spot)));
|
||||
}),
|
||||
_ => resp.on_hover_text(shown),
|
||||
};
|
||||
if resp.clicked() {
|
||||
ev.clicked = Some((e.clone(), ui.input(|i| i.modifiers)));
|
||||
}
|
||||
}
|
||||
|
||||
/// A drag held over the tile or row `resp` of entry `e` shows where it would land (before or after
|
||||
/// it by the pointer's half, or into a folder); a release there sets the drop.
|
||||
fn tile_drop(ui: &Ui, resp: &Response, e: &Entry, list: bool, ev: &mut TileEvents) {
|
||||
let Some(d) = held(resp) else { return };
|
||||
let PanelDrag::Paint { rows, .. } = &*d else { return };
|
||||
let name = e.name();
|
||||
ev.over = true;
|
||||
// Swatches dropped on one of themselves stay where they are.
|
||||
if rows.as_ref().is_some_and(|r| r.names.iter().any(|n| n == name)) {
|
||||
resp.dnd_release_payload::<PanelDrag>();
|
||||
return;
|
||||
}
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let r = resp.rect;
|
||||
let at = ui.input(|i| i.pointer.interact_pos()).unwrap_or(r.center());
|
||||
let after = if list { at.y > r.center().y } else { at.x > r.center().x };
|
||||
// A paint from elsewhere, or swatches over a folder, go into it; otherwise a bar marks the slot.
|
||||
if rows.as_ref().is_none_or(|d| e.is_folder() && !d.groups) {
|
||||
ui.painter().rect_stroke(r.expand(1.0), 0.0, Stroke::new(1.5, t.accent), StrokeKind::Outside);
|
||||
} else if list {
|
||||
let y = if after { r.bottom() } else { r.top() };
|
||||
ui.painter().line_segment([pos2(r.left(), y), pos2(r.right(), y)], Stroke::new(2.0, t.accent));
|
||||
} else {
|
||||
let x = if after { r.right() + 0.75 } else { r.left() - 0.75 };
|
||||
ui.painter().line_segment([pos2(x, r.top() - 1.0), pos2(x, r.bottom() + 1.0)], Stroke::new(2.0, t.accent));
|
||||
}
|
||||
if resp.dnd_release_payload::<PanelDrag>().is_some() {
|
||||
ev.drop = Drop::of(&d, Some(name.to_string()), after, crate::panels::alt_held(ui));
|
||||
}
|
||||
}
|
||||
|
||||
/// Drops between and after the tiles (`zone`: the list's viewport) go to the end of the ungrouped
|
||||
/// swatches (colour groups to the end of the groups); the list is outlined while one is held there.
|
||||
pub(super) fn zone_input(ui: &Ui, zone: &Response, ev: &mut TileEvents) {
|
||||
let Some(d) = held(zone) else { return };
|
||||
if !ev.over {
|
||||
ui.painter().rect_stroke(zone.rect, 0.0, Stroke::new(1.5, Tokens::get(ui.ctx()).accent), StrokeKind::Inside);
|
||||
}
|
||||
if zone.dnd_release_payload::<PanelDrag>().is_some() {
|
||||
ev.drop = Drop::of(&d, None, false, false);
|
||||
}
|
||||
}
|
||||
|
||||
/// The colour group `name` is, or the one swatch `name` belongs to.
|
||||
pub(super) fn group_of(d: &Document, name: &str) -> Option<String> {
|
||||
if d.swatch_groups.iter().any(|g| g.name == name) {
|
||||
return Some(name.to_string());
|
||||
}
|
||||
d.swatch_group_of(name).map(|g| d.swatch_groups[g].name.clone())
|
||||
}
|
||||
|
||||
/// `swatch.move` params for dropping `names` before or `after` row `target`, into folder `target`
|
||||
/// (swatches), or at the end (`target` None); `None` when nothing would move. Only solid colours go
|
||||
/// into colour groups; None and Registration stay put.
|
||||
fn move_params(d: &Document, names: &[String], target: Option<&str>, after: bool) -> Option<Value> {
|
||||
if target.is_some_and(|t| names.iter().any(|n| n == t)) {
|
||||
return None;
|
||||
}
|
||||
let group_index = |n: &str| d.swatch_groups.iter().position(|g| g.name == n);
|
||||
if names.iter().all(|n| group_index(n).is_some()) {
|
||||
let to = match target {
|
||||
None => d.swatch_groups.len(),
|
||||
// Over a group's folder or one of its swatches: next to that group; else before the first.
|
||||
Some(t) => group_index(t).or_else(|| d.swatch_group_of(t)).map_or(0, |i| i + usize::from(after)),
|
||||
};
|
||||
return Some(json!({"names": names, "to": to}));
|
||||
}
|
||||
let (group, to) = match target {
|
||||
Some(t) if group_index(t).is_some() => (Some(t), None),
|
||||
Some(REGISTRATION) => (None, Some(d.swatches.iter().position(|w| !w.paint.is_none()).unwrap_or(0))),
|
||||
Some(t) => {
|
||||
let (group, list) = match d.swatch_group_of(t) {
|
||||
Some(g) => (Some(d.swatch_groups[g].name.as_str()), &d.swatch_groups[g].swatches),
|
||||
None => (None, &*d.swatches),
|
||||
};
|
||||
(group, list.iter().position(|w| w.name == t).map(|i| i + usize::from(after)))
|
||||
}
|
||||
None => (None, None),
|
||||
};
|
||||
let movable: Vec<&String> =
|
||||
names.iter().filter(|n| d.swatch(n).is_some_and(|w| !w.paint.is_none() && (group.is_none() || w.paint.color().is_some()))).collect();
|
||||
(!movable.is_empty()).then(|| json!({"names": movable, "to": to, "group": group}))
|
||||
}
|
||||
|
||||
/// Are both paints colours, gradients or patterns?
|
||||
fn same_kind(a: &Paint, b: &Paint) -> bool {
|
||||
std::mem::discriminant(a) == std::mem::discriminant(b) && !a.is_none()
|
||||
}
|
||||
|
||||
/// Can swatch `with` give its paint to swatch `target`? Both are swatches of one kind, neither is
|
||||
/// None or Registration, and `target` is not a tint swatch (a tint swatch only takes a new name).
|
||||
fn replaces(d: &Document, with: &str, target: &str) -> bool {
|
||||
match (d.swatch(with), d.swatch(target)) {
|
||||
(Some(w), Some(t)) => with != target && !w.is_reserved() && !t.is_reserved() && t.tint_of().is_none() && same_kind(&t.paint, &w.paint),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Run a drop on the panel: move the swatches, give the paint of a swatch Alt-dropped on another of
|
||||
/// its kind to that one, or make a swatch of the proxy's paint (a color dropped on a folder or a
|
||||
/// grouped swatch joins that group).
|
||||
pub(super) fn apply_drop(app: &mut VectorcraftApp, drop: Drop) {
|
||||
let Some(d) = app.session.active().map(|st| &st.doc) else { return };
|
||||
let (cmd, params) = match drop {
|
||||
// Alt: the swatch grabbed replaces the color, gradient or pattern of the swatch of its kind
|
||||
// it is dropped on, which keeps its name and place.
|
||||
Drop::Move { with: Some(w), target: Some(t), .. } if replaces(d, &w, &t) => ("swatch.edit", json!({"name": t, "paint": {"swatch": w}})),
|
||||
Drop::Move { names, target, after, .. } => match move_params(d, &names, target.as_deref(), after) {
|
||||
Some(p) => ("swatch.move", p),
|
||||
None => return,
|
||||
},
|
||||
// Alt: an edited gradient (or colour) replaces the swatch it is dropped on.
|
||||
Drop::New { paint, target: Some(t), replace: true } if d.swatch(&t).is_some_and(|w| w.tint_of().is_none() && same_kind(&w.paint, &paint)) => {
|
||||
("swatch.edit", json!({"name": t, "paint": crate::panels::swatch_paint_params(&paint)}))
|
||||
}
|
||||
Drop::New { paint, target, .. } => {
|
||||
let mut p = crate::panels::swatch_paint_params(&paint);
|
||||
if paint.color().is_some()
|
||||
&& let Some(g) = target.and_then(|t| group_of(d, &t))
|
||||
{
|
||||
p["group"] = json!(g);
|
||||
}
|
||||
("swatch.new", p)
|
||||
}
|
||||
Drop::Pattern { ids } => ("object.pattern.make", json!({"ids": ids, "edit": false})),
|
||||
};
|
||||
if let Err(e) = app.run(cmd, params) {
|
||||
app.status(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// While a panel drags something onto art, a chip follows the pointer: the dragged paint (swatches,
|
||||
/// a Fill/Stroke proxy, the Gradient panel's thumbnail), the fill of the object whose appearance
|
||||
/// the Appearance panel's thumbnail carries (or of the first art dragged off the canvas), or the
|
||||
/// thumbnail of a dragged graphic style.
|
||||
pub(crate) fn drag_preview(app: &VectorcraftApp, ctx: &egui::Context) {
|
||||
let Some(d) = egui::DragAndDrop::payload::<PanelDrag>(ctx) else { return };
|
||||
let fill_of = |id: &vectorcraft_doc::NodeId| app.session.active().and_then(|st| st.doc.node(*id)).map(|n| n.appearance.fill_paint());
|
||||
let (paint, registration) = match &*d {
|
||||
// Colour groups paint nothing.
|
||||
PanelDrag::Paint { params, .. } if params.is_null() => return,
|
||||
PanelDrag::Paint { paint, rows, .. } => (std::borrow::Cow::Borrowed(paint), rows.as_ref().is_some_and(|r| r.grabbed == REGISTRATION)),
|
||||
PanelDrag::Appearance(id) => match fill_of(id) {
|
||||
Some(p) => (std::borrow::Cow::Owned(p), false),
|
||||
None => return,
|
||||
},
|
||||
PanelDrag::Art(ids) => match ids.first().and_then(fill_of) {
|
||||
Some(p) => (std::borrow::Cow::Owned(p), false),
|
||||
None => return,
|
||||
},
|
||||
PanelDrag::GraphicStyle(name) => return pointer_chip(ctx, |ui, r| crate::panels::graphic_styles::paint_style(app, ui, r, name)),
|
||||
PanelDrag::Symbol(name) => match app.session.active() {
|
||||
Some(st) => {
|
||||
return pointer_chip(ctx, |ui, r| crate::panels::symbols::chip(ui, &st.doc, &crate::panels::symbols::document_symbols(st), r, name));
|
||||
}
|
||||
None => return,
|
||||
},
|
||||
PanelDrag::LibrarySymbol { library, name } => match vectorcraft_engine::cmd::symbollib::library(&app.session, library) {
|
||||
Some((_, lib)) => {
|
||||
return pointer_chip(ctx, |ui, r| {
|
||||
crate::panels::symbols::chip(ui, &lib.doc, &crate::panels::symbols::library_symbols(&lib), r, name)
|
||||
});
|
||||
}
|
||||
None => return,
|
||||
},
|
||||
PanelDrag::Brush { name, .. } => match app.session.active() {
|
||||
Some(st) => return pointer_chip(ctx, |ui, r| crate::panels::brushes::document_chip(ui, r, st, name)),
|
||||
None => return,
|
||||
},
|
||||
PanelDrag::LibraryBrush { library, name } => match vectorcraft_engine::cmd::brushlib::library(&app.session, library) {
|
||||
Some((_, lib)) => return pointer_chip(ctx, |ui, r| crate::panels::brushes::library_chip(ui, r, &lib, name)),
|
||||
None => return,
|
||||
},
|
||||
PanelDrag::LibraryGraphic { library, item } => return pointer_chip(ctx, |ui, r| crate::panels::libraries::chip(app, ui, r, library, item)),
|
||||
PanelDrag::LibraryItem { .. } | PanelDrag::LibraryGroup { .. } => return,
|
||||
};
|
||||
pointer_chip(ctx, |ui, r| {
|
||||
if registration {
|
||||
draw_registration(ui, r);
|
||||
} else {
|
||||
swatch_tile(ui, r, &paint, false, false);
|
||||
pattern_thumb(app, ui, r.shrink(1.0), &paint);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// A 16-point chip `draw` paints, following the pointer.
|
||||
fn pointer_chip(ctx: &egui::Context, draw: impl FnOnce(&Ui, Rect)) {
|
||||
let Some(at) = ctx.pointer_hover_pos() else { return };
|
||||
let area = egui::Area::new(egui::Id::unique("swatch-drag-preview")).order(egui::Order::Tooltip).fixed_pos(at + vec2(12.0, 12.0));
|
||||
area.interactable(false).show(ctx, |ui| {
|
||||
let (r, _) = ui.allocate_exact_size(vec2(16.0, 16.0), Sense::hover());
|
||||
draw(ui, r);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
use super::actions::deletable;
|
||||
use super::dnd::selection;
|
||||
use super::tiles::{describe, global_mark, list_icons};
|
||||
use super::*;
|
||||
|
||||
fn library_refs(libs: Vec<swatchlib::LibraryInfo>) -> Vec<LibraryRef> {
|
||||
libs.into_iter().map(|l| LibraryRef { submenu: library_panel::submenu(l.category), id: l.id, name: l.name }).collect()
|
||||
}
|
||||
|
||||
/// The swatch libraries with solid colours (not the gradient-only ones), for Limit to Library.
|
||||
pub(crate) fn colour_libraries(app: &VectorcraftApp) -> Vec<LibraryRef> {
|
||||
let has_colours = |id: &str| swatchlib::library(&app.session, id).is_some_and(|(_, lib)| lib.iter().any(|w| w.paint.color().is_some()));
|
||||
library_refs(swatchlib::libraries(&app.session).into_iter().filter(|l| l.category != "gradients" && has_colours(&l.id)).collect())
|
||||
}
|
||||
|
||||
/// What Limit to Library menus call the document's own swatches ([`swatchlib::DOCUMENT_SWATCHES`]).
|
||||
pub(crate) const DOCUMENT_SWATCHES: &str = "Document Swatches";
|
||||
|
||||
/// The name of Limit to Library choice `key` (a library id or [`swatchlib::DOCUMENT_SWATCHES`]);
|
||||
/// `None` for no limit or a library that is gone.
|
||||
pub(crate) fn limit_name(app: &VectorcraftApp, key: &str) -> Option<String> {
|
||||
match key {
|
||||
"" => None,
|
||||
swatchlib::DOCUMENT_SWATCHES => Some(DOCUMENT_SWATCHES.into()),
|
||||
_ => swatchlib::library(&app.session, key).map(|(info, _)| info.name),
|
||||
}
|
||||
}
|
||||
|
||||
/// The Limit to Library key for `l`: a library's id (from its id or name), or
|
||||
/// [`swatchlib::DOCUMENT_SWATCHES`] as it is.
|
||||
pub(crate) fn limit_key(app: &VectorcraftApp, l: &str) -> Result<String, String> {
|
||||
if l == swatchlib::DOCUMENT_SWATCHES {
|
||||
return Ok(l.into());
|
||||
}
|
||||
swatchlib::library(&app.session, l).map(|(info, _)| info.id).ok_or_else(|| format!("no swatch library `{l}` (see swatch.library.list)"))
|
||||
}
|
||||
|
||||
/// Swatch libraries in library panels (engine: `swatch.library.*`).
|
||||
pub(crate) struct SwatchLibraries;
|
||||
|
||||
impl LibraryKind for SwatchLibraries {
|
||||
const KIND: &'static str = "swatches";
|
||||
const OPEN: &'static str = "window.swatchLibrary";
|
||||
const LIST: &'static str = "swatch.library.list";
|
||||
const LOAD: &'static str = "swatch.library.load";
|
||||
const MENU: &'static str = "Swatch Libraries Menu";
|
||||
const ADD: &'static str = "Add to Swatches";
|
||||
type Lib = Arc<SwatchLibrary>;
|
||||
type Item = Swatch;
|
||||
|
||||
fn list(app: &VectorcraftApp) -> Vec<LibraryRef> {
|
||||
library_refs(swatchlib::libraries(&app.session))
|
||||
}
|
||||
fn get(app: &VectorcraftApp, id: &str) -> Option<(String, Self::Lib)> {
|
||||
swatchlib::library(&app.session, id).map(|(info, lib)| (info.name, lib))
|
||||
}
|
||||
fn find(app: &VectorcraftApp, key: &str) -> Option<(String, String, usize)> {
|
||||
swatchlib::library(&app.session, key).map(|(info, lib)| (info.id, info.name, lib.len()))
|
||||
}
|
||||
fn path(app: &VectorcraftApp, id: &str) -> Option<String> {
|
||||
app.session.swatch_libraries.path(id).map(str::to_string)
|
||||
}
|
||||
/// Ungrouped swatches, then each colour group's folder and swatches (a group whose name
|
||||
/// matches shows all of them), as in the Swatches panel.
|
||||
fn rows<'a>(lib: &'a Self::Lib, query: &str) -> Vec<Row<'a, Swatch>> {
|
||||
let found = |n: &str| query.is_empty() || n.to_lowercase().contains(query);
|
||||
let row = |w: &'a Swatch| Row { name: &w.name, item: Some(w) };
|
||||
let mut out: Vec<Row<Swatch>> = lib.swatches.iter().filter(|w| found(&w.name)).map(row).collect();
|
||||
for g in &lib.groups {
|
||||
let all = found(&g.name);
|
||||
let mut items = g.swatches.iter().filter(|w| all || found(&w.name)).map(row).peekable();
|
||||
if items.peek().is_some() {
|
||||
out.push(Row { name: &g.name, item: None });
|
||||
out.extend(items);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
fn draw(_: &VectorcraftApp, ui: &Ui, r: Rect, w: &Swatch, selected: bool, hovered: bool) {
|
||||
swatch_tile(ui, r, &w.paint, selected, hovered);
|
||||
if w.global {
|
||||
global_mark(ui, r, w.spot);
|
||||
}
|
||||
}
|
||||
fn row_icons(ui: &Ui, r: Rect, w: &Swatch) {
|
||||
list_icons(ui, r, &w.paint, w.global, w.spot);
|
||||
}
|
||||
fn describe(w: &Swatch) -> String {
|
||||
describe(&w.paint, w.global, w.spot)
|
||||
}
|
||||
/// Adds the swatch and applies it to the active proxy (Alt: the inactive one), as one step.
|
||||
fn click(app: &mut VectorcraftApp, ui: &Ui, id: &str, name: &str) {
|
||||
let alt = crate::panels::alt_held(ui);
|
||||
let apply = if crate::panels::proxy_cmd(app, alt) == "paint.setFill" { "fill" } else { "stroke" };
|
||||
add_from_library(app, json!({"library": id, "names": [name], "apply": apply, "focus": !alt}));
|
||||
}
|
||||
fn add(app: &mut VectorcraftApp, id: &str, names: Vec<String>) {
|
||||
add_from_library(app, json!({"library": id, "names": names}));
|
||||
}
|
||||
fn menu_head(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
if menu_item(ui, tl!("Default Swatches"), app.session.active().is_some(), false) {
|
||||
app.run("swatch.resetDefaults", json!({})).ok();
|
||||
}
|
||||
ui.separator();
|
||||
}
|
||||
fn menu_tail(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
ui.separator();
|
||||
// As a command, so its file dialog, shown off the UI thread, loads the library when it
|
||||
// answers.
|
||||
if menu_item(ui, tl!("Other Library…"), true, false)
|
||||
&& let Err(e) = app.run("window.swatchLibrary.other", json!({}))
|
||||
{
|
||||
app.status(e);
|
||||
}
|
||||
if menu_item(ui, tl!("Save Swatch Library…"), app.session.active().is_some(), false) {
|
||||
let names = deletable(&selection(app, ui));
|
||||
app.run("ui.saveSwatchLibrary", json!({ "names": names })).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn add_from_library(app: &mut VectorcraftApp, params: Value) {
|
||||
if let Err(e) = app.run("swatch.library.add", params) {
|
||||
app.status(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// `window.swatchLibrary {library}`: open a swatch library in its own panel (`library` null closes
|
||||
/// every swatch library panel).
|
||||
pub(crate) fn open_library(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
|
||||
library_panel::open_command::<SwatchLibraries>(app, p)
|
||||
}
|
||||
|
||||
/// `swatch.library.load` params, then open the library in its panel.
|
||||
pub(crate) fn load_library(app: &mut VectorcraftApp, params: Value) -> Result<Value, String> {
|
||||
let r = app.run("swatch.library.load", params)?;
|
||||
open_library(app, &json!({ "library": r["library"] }))
|
||||
}
|
||||
|
||||
/// Other Library…: load the library (or document) at `path`, else one picked in an open dialog
|
||||
/// (on the web the picked file arrives later and opens through [`crate::io::open_bytes`]).
|
||||
pub(crate) fn other_library(app: &mut VectorcraftApp, path: Option<String>) -> Result<Value, String> {
|
||||
match library_panel::pick_library_file(app, path, library_panel::library_pick_filters(SwatchLibraries::KIND))? {
|
||||
Some(path) => load_library(app, json!({ "path": path })),
|
||||
None => Ok(Value::Null),
|
||||
}
|
||||
}
|
||||
|
||||
/// The id prefix of the Window → Swatch Libraries → User Defined slots.
|
||||
pub(crate) const USER_SLOT: &str = "window.userSwatchLibrary";
|
||||
|
||||
/// The User Defined library slot `id` (`window.userSwatchLibrary3`) stands for.
|
||||
pub(crate) fn user_library(app: &VectorcraftApp, id: &str) -> Option<swatchlib::LibraryInfo> {
|
||||
library_panel::user_slot(id, USER_SLOT, swatchlib::libraries(&app.session))
|
||||
}
|
||||
|
||||
/// Window → Swatch Libraries.
|
||||
pub(crate) fn window_menu() -> Vec<Item> {
|
||||
const SLOTS: [&str; 10] = [
|
||||
"window.userSwatchLibrary1",
|
||||
"window.userSwatchLibrary2",
|
||||
"window.userSwatchLibrary3",
|
||||
"window.userSwatchLibrary4",
|
||||
"window.userSwatchLibrary5",
|
||||
"window.userSwatchLibrary6",
|
||||
"window.userSwatchLibrary7",
|
||||
"window.userSwatchLibrary8",
|
||||
"window.userSwatchLibrary9",
|
||||
"window.userSwatchLibrary10",
|
||||
];
|
||||
let open = |b: &'static BuiltinLibrary| Item::Cmd(b.name, "window.swatchLibrary", json!({ "library": b.id }));
|
||||
let mut items = vec![Item::Cmd("Default Swatches", "swatch.resetDefaults", Value::Null), Item::Sep];
|
||||
items.extend(SWATCH_LIBRARIES.iter().map(open));
|
||||
items.extend([
|
||||
Item::Sub("Gradients", GRADIENT_LIBRARIES.iter().map(open).collect()),
|
||||
Item::Sep,
|
||||
Item::Sub("User Defined", SLOTS.iter().map(|id| Item::Cmd("User Library", id, Value::Null)).collect()),
|
||||
Item::Sep,
|
||||
Item::Cmd("Other Library…", "window.swatchLibrary.other", Value::Null),
|
||||
Item::Cmd("Save Swatch Library…", "ui.saveSwatchLibrary", Value::Null),
|
||||
]);
|
||||
items
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
use super::*;
|
||||
|
||||
/// A pattern swatch drawn as a rendered tile (cached by the definition's identity and size).
|
||||
pub(super) fn pattern_thumb(app: &VectorcraftApp, ui: &Ui, r: Rect, paint: &Paint) {
|
||||
use std::collections::HashMap;
|
||||
type Key = (String, Vec<usize>, String, u32);
|
||||
thread_local! {
|
||||
static CACHE: crate::graphics::TexCache<HashMap<Key, Option<egui::TextureHandle>>> = crate::graphics::TexCache::default();
|
||||
}
|
||||
let Paint::Pattern { pattern, .. } = paint else { return };
|
||||
let Some(st) = app.session.active() else { return };
|
||||
let Some(def) = st.doc.pattern(pattern) else { return };
|
||||
let px = (r.width() * ui.ctx().pixels_per_point()).round().max(4.0) as u32;
|
||||
let key = (
|
||||
pattern.clone(),
|
||||
def.art.iter().map(|a| std::sync::Arc::as_ptr(a) as usize).collect(),
|
||||
format!("{:?}{:?}{:?}", def.tile, def.tile_type, def.overlap),
|
||||
px,
|
||||
);
|
||||
let tex = CACHE.with(|c| c.borrow().get(&key).cloned()).unwrap_or_else(|| {
|
||||
let tex = vectorcraft_render::render_pattern_swatch(&st.doc, pattern, px).map(|img| {
|
||||
let color = crate::canvas::color_image(&img);
|
||||
ui.ctx().load_texture(format!("pattern-swatch-{pattern}-{px}"), color, egui::TextureOptions::LINEAR)
|
||||
});
|
||||
CACHE.with(|c| {
|
||||
let mut c = c.borrow_mut();
|
||||
if c.len() > 256 {
|
||||
c.clear();
|
||||
}
|
||||
c.insert(key, tex.clone());
|
||||
});
|
||||
tex
|
||||
});
|
||||
if let Some(tex) = tex {
|
||||
ui.painter().rect_filled(r, 0.0, egui::Color32::WHITE);
|
||||
ui.painter().image(tex.id(), r, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), egui::Color32::WHITE);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn draw_registration(ui: &Ui, r: Rect) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
ui.painter().rect_filled(r, 0.0, egui::Color32::WHITE);
|
||||
let c = r.center();
|
||||
let rad = r.width() * 0.28;
|
||||
let s = Stroke::new(1.0, egui::Color32::BLACK);
|
||||
ui.painter().circle_stroke(c, rad, s);
|
||||
ui.painter().line_segment([pos2(r.left() + 2.0, c.y), pos2(r.right() - 2.0, c.y)], s);
|
||||
ui.painter().line_segment([pos2(c.x, r.top() + 2.0), pos2(c.x, r.bottom() - 2.0)], s);
|
||||
ui.painter().rect_stroke(r, 0.0, Stroke::new(1.0, t.border), StrokeKind::Inside);
|
||||
}
|
||||
|
||||
pub(crate) fn draw_folder(ui: &Ui, r: Rect) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
icons::paint(ui, "dc-folder", r.expand(1.0), t.icon);
|
||||
}
|
||||
|
||||
/// The ink colours of the CMYK glyph (cyan, magenta, yellow, black) and the RGB glyph's bars.
|
||||
const CMYK_INKS: [Color32; 4] = [Color32::from_rgb(0, 174, 239), Color32::from_rgb(236, 0, 140), Color32::from_rgb(255, 242, 0), Color32::BLACK];
|
||||
|
||||
const RGB_BARS: [Color32; 3] = [Color32::from_rgb(255, 0, 0), Color32::from_rgb(0, 200, 0), Color32::from_rgb(0, 0, 255)];
|
||||
|
||||
/// Lab's opponent axes: green | red (a) over blue | yellow (b).
|
||||
const LAB_AXES: [Color32; 4] =
|
||||
[Color32::from_rgb(0, 166, 81), Color32::from_rgb(230, 30, 60), Color32::from_rgb(0, 90, 220), Color32::from_rgb(250, 205, 0)];
|
||||
|
||||
/// A swatch's kind in words, for list tooltips ("Global Process Color, CMYK").
|
||||
pub(crate) fn describe(paint: &Paint, global: bool, spot: bool) -> String {
|
||||
match paint {
|
||||
Paint::Solid { color, .. } => {
|
||||
let kind = if spot {
|
||||
tl!("Spot Color")
|
||||
} else if global {
|
||||
tl!("Global Process Color")
|
||||
} else {
|
||||
tl!("Process Color")
|
||||
};
|
||||
crate::i18n::fmt(tl!("{kind}, {model}"), &[("kind", kind), ("model", color.model_name())])
|
||||
}
|
||||
Paint::Gradient(g) => crate::i18n::fmt(
|
||||
tl!("{kind} Gradient"),
|
||||
&[("kind", crate::i18n::tr_ctx(crate::i18n::current(), "gradient-kind", g.gradient.kind.label()))],
|
||||
),
|
||||
Paint::Pattern { .. } => tl!("Pattern").into(),
|
||||
Paint::None => tl!("None").into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The kind and colour-mode icons at the right of list row `r`: spot (a dot in a ring), global (a
|
||||
/// square with a filled corner) or process colours (a square), gradients and patterns; then a
|
||||
/// colour's model (CMYK as four ink quarters, RGB as three bars, Gray as a grey square, Lab as its
|
||||
/// two opponent axes).
|
||||
pub(crate) fn list_icons(ui: &Ui, r: Rect, paint: &Paint, global: bool, spot: bool) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let p = ui.painter();
|
||||
let mode = Rect::from_center_size(r.right_center() - vec2(12.0, 0.0), vec2(10.0, 10.0));
|
||||
let kind = mode.translate(vec2(-16.0, 0.0));
|
||||
let line = Stroke::new(1.0, t.icon);
|
||||
match paint {
|
||||
Paint::Solid { color, .. } => {
|
||||
if spot {
|
||||
p.circle_stroke(kind.center(), 4.5, line);
|
||||
p.circle_filled(kind.center(), 1.8, t.icon);
|
||||
} else {
|
||||
p.rect_stroke(kind, 0.0, line, StrokeKind::Inside);
|
||||
if global {
|
||||
let c = kind.right_bottom();
|
||||
p.add(Shape::convex_polygon(vec![c, c - vec2(6.0, 0.0), c - vec2(0.0, 6.0)], t.icon, Stroke::NONE));
|
||||
}
|
||||
}
|
||||
let quarters = |colors: [Color32; 4]| {
|
||||
let q = mode.size() / 2.0;
|
||||
for (i, c) in colors.into_iter().enumerate() {
|
||||
p.rect_filled(Rect::from_min_size(mode.min + vec2((i % 2) as f32 * q.x, (i / 2) as f32 * q.y), q), 0.0, c);
|
||||
}
|
||||
};
|
||||
match color {
|
||||
Color::Cmyk { .. } => quarters(CMYK_INKS),
|
||||
Color::Lab { .. } => quarters(LAB_AXES),
|
||||
Color::Rgb { .. } => {
|
||||
let w = mode.width() / 3.0;
|
||||
for (i, bar) in RGB_BARS.into_iter().enumerate() {
|
||||
p.rect_filled(Rect::from_min_size(mode.min + vec2(i as f32 * w, 0.0), vec2(w, mode.height())), 0.0, bar);
|
||||
}
|
||||
}
|
||||
Color::Gray { .. } => {
|
||||
p.rect_filled(mode, 0.0, Color32::from_gray(128));
|
||||
}
|
||||
}
|
||||
p.rect_stroke(mode, 0.0, Stroke::new(1.0, t.border), StrokeKind::Outside);
|
||||
}
|
||||
Paint::Gradient(g) => {
|
||||
let icon = match g.gradient.kind {
|
||||
GradientKind::Linear => "dc-grad-linear",
|
||||
GradientKind::Radial => "dc-grad-radial",
|
||||
GradientKind::Freeform => "dc-grad-freeform",
|
||||
};
|
||||
icons::paint(ui, icon, kind.expand(1.0), t.icon);
|
||||
}
|
||||
Paint::Pattern { .. } => icons::paint(ui, "grid-3x3", kind.expand(1.0), t.icon),
|
||||
Paint::None => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// The thumbnail cells of `items` for `view` in the full available width, row by row; a folder
|
||||
/// (`is_folder`) starts a new row.
|
||||
pub(crate) fn tile_grid<'a, T>(ui: &mut Ui, items: &'a [T], view: View, is_folder: impl Fn(&T) -> bool) -> Vec<(&'a T, Rect)> {
|
||||
let (tile, pitch) = view.tile();
|
||||
let w = ui.available_width();
|
||||
let per_row = ((w - 2.0) / pitch).floor().max(1.0) as usize;
|
||||
let mut rows: Vec<Vec<&T>> = vec![vec![]];
|
||||
for e in items {
|
||||
match rows.last_mut() {
|
||||
Some(row) if row.len() < per_row && (row.is_empty() || !is_folder(e)) => row.push(e),
|
||||
_ => rows.push(vec![e]),
|
||||
}
|
||||
}
|
||||
let mut out = Vec::with_capacity(items.len());
|
||||
for row in rows {
|
||||
let (r, _) = ui.allocate_exact_size(vec2(w, pitch), Sense::hover());
|
||||
let cell = |c: usize| Rect::from_min_size(r.min + vec2(1.0 + c as f32 * pitch, (pitch - tile) / 2.0), vec2(tile, tile));
|
||||
out.extend(row.into_iter().enumerate().map(|(c, e)| (e, cell(c))));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// A list-view row for `view` taking clicks and drags as `id`, shaded when `selected` or hovered
|
||||
/// → (the row, its response, the chip at its left).
|
||||
pub(crate) fn list_row(ui: &mut Ui, id: egui::Id, view: View, selected: bool) -> (Rect, Response, Rect) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let (tile, pitch) = view.tile();
|
||||
let (_, r) = ui.allocate_space(vec2(ui.available_width(), pitch));
|
||||
let resp = ui.interact(r, id, Sense::click_and_drag());
|
||||
if selected {
|
||||
ui.painter().rect_filled(r, 0.0, t.row_selected);
|
||||
} else if resp.hovered() {
|
||||
ui.painter().rect_filled(r, 0.0, t.hover);
|
||||
}
|
||||
(r, resp, Rect::from_min_size(r.left_center() + vec2(4.0, -tile / 2.0), vec2(tile, tile)))
|
||||
}
|
||||
|
||||
/// A list row's name, right of its `chip`.
|
||||
pub(crate) fn list_row_name(ui: &Ui, r: Rect, chip: Rect, name: &str) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
ui.painter().text(pos2(chip.right() + 8.0, r.center().y), egui::Align2::LEFT_CENTER, name, egui::FontId::proportional(12.0), t.text);
|
||||
}
|
||||
|
||||
/// The white corner of a global colour's tile; a spot colour has a dot in it.
|
||||
pub(crate) fn global_mark(ui: &Ui, cell: Rect, spot: bool) {
|
||||
let k = cell.shrink(1.0).right_bottom();
|
||||
let corner = vec![k, k - vec2(5.0, 0.0), k - vec2(0.0, 5.0)];
|
||||
ui.painter().add(Shape::convex_polygon(corner, Color32::WHITE, Stroke::NONE));
|
||||
if spot {
|
||||
ui.painter().circle_filled(k - vec2(1.6, 1.6), 1.0, Color32::BLACK);
|
||||
}
|
||||
}
|
||||
|
||||
/// A colour group's folder in the thumbnail view, outlined when `selected`.
|
||||
pub(crate) fn folder_tile(ui: &Ui, cell: Rect, selected: bool) {
|
||||
draw_folder(ui, cell);
|
||||
if selected {
|
||||
ui.painter().rect_stroke(cell.expand(1.0), 0.0, Stroke::new(1.5, Tokens::get(ui.ctx()).accent), StrokeKind::Outside);
|
||||
}
|
||||
}
|
||||
|
||||
/// The interaction id of a tile or list row (stable per swatch or group name and host `salt`).
|
||||
pub(super) fn tile_id(salt: &'static str, e: &Entry) -> egui::Id {
|
||||
egui::Id::unique((salt, e.name()))
|
||||
}
|
||||
Reference in New Issue
Block a user