mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Merge pull request #2714 from storytold/fix/2603-library-panels
Fix #2603: a panel per swatch library, with Persistent
This commit is contained in:
@@ -875,6 +875,8 @@ fn main() -> std::process::ExitCode {
|
||||
app.session.swatch_libraries.set_user_dir(swatches);
|
||||
let styles = prefs_path().and_then(|p| Some(p.parent()?.join("Graphic Styles").to_string_lossy().to_string()));
|
||||
app.session.style_libraries.set_user_dir(styles);
|
||||
// With the User Defined libraries listed, the persistent library panels reopen.
|
||||
vectorcraft_ui_egui::panels::library_panel::restore(&mut app);
|
||||
// So do the Libraries panel's libraries; runs without preferences (agents' test
|
||||
// runs) keep theirs for the session only, never touching the user's.
|
||||
if prefs_enabled() {
|
||||
|
||||
@@ -305,12 +305,20 @@ pub(crate) fn panel_tabs(
|
||||
/// A panel popped out next to the icon column: an icon panel, or one of the tabbed group's panels
|
||||
/// while the dock is collapsed. Dragging its title floats it.
|
||||
pub fn floating_panel(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
let Some((id, label)) = app.ui.open_panel.as_deref().and_then(|p| crate::state::all_panels().find(|(q, _)| *q == p)) else { return };
|
||||
let open = app.ui.open_panel.as_deref();
|
||||
let Some(id) =
|
||||
open.and_then(|p| crate::state::all_panels().map(|(q, _)| q).find(|q| *q == p).or_else(|| crate::panels::library_panel::intern(p)))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let tab = DockTab::from_id(id);
|
||||
// Expanded, the group shows its panels in the dock itself; a floating panel shows in its group.
|
||||
if tab.is_some() && !app.ui.dock_collapsed || floating::group_of(&app.ui, id).is_some() {
|
||||
return;
|
||||
}
|
||||
// A library panel (in a collapsed dock) is titled by its library's name.
|
||||
let label = crate::panels::library_panel::tab_label(app, id)
|
||||
.unwrap_or_else(|| crate::state::all_panels().find(|(q, _)| *q == id).map_or(id, |(_, label)| tl!(label)).to_string());
|
||||
let t = Tokens::get(ctx);
|
||||
let screen = ctx.content_rect();
|
||||
// Pinned by its right edge, 6 points left of the icon column wherever the dock's width (or its
|
||||
@@ -338,12 +346,10 @@ pub fn floating_panel(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
ui.set_width(width);
|
||||
let (strip, _) = ui.allocate_exact_size(vec2(width, 26.0), Sense::hover());
|
||||
ui.painter().rect_filled(strip, CornerRadius { nw: 4, ne: 4, sw: 0, se: 0 }, t.panel_darker);
|
||||
let title = Rect::from_min_size(
|
||||
strip.min,
|
||||
vec2(ui.painter().layout_no_wrap(tl!(label).to_string(), theme::semibold(12.0), t.text).size().x + 24.0, 26.0),
|
||||
);
|
||||
let title =
|
||||
Rect::from_min_size(strip.min, vec2(ui.painter().layout_no_wrap(label.clone(), theme::semibold(12.0), t.text).size().x + 24.0, 26.0));
|
||||
ui.painter().rect_filled(title, CornerRadius { nw: 4, ne: 0, sw: 0, se: 0 }, t.panel);
|
||||
ui.painter().text(title.left_center() + vec2(12.0, 0.0), egui::Align2::LEFT_CENTER, tl!(label), theme::semibold(12.0), t.text);
|
||||
ui.painter().text(title.left_center() + vec2(12.0, 0.0), egui::Align2::LEFT_CENTER, &label, theme::semibold(12.0), t.text);
|
||||
let close = Rect::from_center_size(strip.right_center() - vec2(13.0, 0.0), vec2(14.0, 14.0));
|
||||
let cr = ui.interact(close, ui.id().with("close-panel"), Sense::click());
|
||||
icons::paint(ui, "chevrons-right", close, if cr.hovered() { t.text } else { t.text_dim });
|
||||
|
||||
@@ -122,9 +122,15 @@ pub(crate) fn group_rect_id(first: &str) -> Id {
|
||||
Id::new(("floating-group-rect", first))
|
||||
}
|
||||
|
||||
/// Panel `id` (`window.panel`) as a static string and its English label, if it is a panel.
|
||||
/// Panel `id` (`window.panel`) as a static string and its English label, if it is a panel (a
|
||||
/// library panel's label is its id: [`label`] gives its library's name).
|
||||
fn panel(id: &str) -> Option<(&'static str, &'static str)> {
|
||||
all_panels().find(|(p, _)| *p == id)
|
||||
all_panels().find(|(p, _)| *p == id).or_else(|| crate::panels::library_panel::intern(id).map(|p| (p, p)))
|
||||
}
|
||||
|
||||
/// Panel `id`'s tab label: its English label, or a library panel's library name.
|
||||
fn label(app: &VectorcraftApp, id: &str) -> String {
|
||||
crate::panels::library_panel::tab_label(app, id).unwrap_or_else(|| panel(id).map_or(id, |(_, label)| label).to_string())
|
||||
}
|
||||
|
||||
/// The floating group holding panel `id`.
|
||||
@@ -816,10 +822,7 @@ fn show_set(app: &mut VectorcraftApp, ctx: &egui::Context, set: &[usize], action
|
||||
let tabs_width: f32 = g
|
||||
.ids
|
||||
.iter()
|
||||
.map(|id| {
|
||||
let label = panel(id).map_or(*id, |(_, label)| label);
|
||||
ui.painter().layout_no_wrap(tl!(label).to_string(), theme::semibold(12.0), t.text).size().x + 24.0
|
||||
})
|
||||
.map(|id| ui.painter().layout_no_wrap(tl!(&label(app, id)).to_string(), theme::semibold(12.0), t.text).size().x + 24.0)
|
||||
.sum();
|
||||
g.ids.iter().map(|id| dock::panel_width(id)).fold(tabs_width + 32.0, f32::max)
|
||||
})
|
||||
@@ -882,7 +885,8 @@ fn draw_groups(
|
||||
let lone = g.ids.len() == 1;
|
||||
let menu = Rect::from_center_size(strip.right_center() - vec2(14.0, 0.0), vec2(16.0, 16.0));
|
||||
let tab_area = Rect::from_min_max(strip.min, pos2((menu.left() - 4.0).max(strip.left()), strip.bottom()));
|
||||
let names: Vec<_> = g.ids.iter().map(|&id| (id, panel(id).map_or(id, |(_, label)| label))).collect();
|
||||
let labels: Vec<String> = g.ids.iter().map(|id| label(app, id)).collect();
|
||||
let names: Vec<_> = g.ids.iter().zip(&labels).map(|(&id, label)| (id, label.as_str())).collect();
|
||||
let selected = g.ids.iter().position(|&id| id == g.active).unwrap_or(0);
|
||||
let out = dock::panel_tabs(ui, &format!("native-set-tabs-{}", g.gi), tab_area, &names, selected, theme::semibold(12.0), false);
|
||||
for (j, resp) in out.responses {
|
||||
|
||||
@@ -5125,3 +5125,5 @@ theme Appearance Външен вид
|
||||
@msg name is empty името е празно
|
||||
@msg no symbol named `{n}` няма символ с име `{n}`
|
||||
Enable Guides for 9-Slice Scaling Включване на водачи за 9-slice мащабиране
|
||||
Persistent Постоянна
|
||||
Library Panel Options Опции на панела с библиотека
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5149,3 +5149,9 @@ mark-style Japanese Japonské
|
||||
@msg a linked image can't become a gradient mesh: embed it first propojený obraz nelze převést na síť přechodu: nejprve jej vložte
|
||||
@msg the image has no usable size or position obraz nemá použitelnou velikost ani polohu
|
||||
@msg the image's pixels can't be read pixely obrazu nelze přečíst
|
||||
Persistent Trvalé
|
||||
Library Panel Options Možnosti panelu knihovny
|
||||
@msg Persistent libraries not found: {files} Trvalé knihovny nebyly nalezeny: {files}
|
||||
@msg `{given}` isn't a library panel (see window.swatchLibrary) `{given}` není panel knihovny (viz window.swatchLibrary)
|
||||
@msg library panel `{given}` isn't open panel knihovny `{given}` není otevřený
|
||||
@msg too many library panels were opened in this session v této relaci bylo otevřeno příliš mnoho panelů knihoven
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5149,3 +5149,5 @@ swatch Bright Violet Kraftig violet
|
||||
Text Orientation Tekstretning
|
||||
Thread Text Sammenkæd tekst
|
||||
Typing Indtastning
|
||||
Persistent Vedvarende
|
||||
Library Panel Options Indstillinger for bibliotekspanel
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4724,3 +4724,9 @@ effect Regular Regelmäßig
|
||||
@msg a linked image can't become a gradient mesh: embed it first ein verknüpftes Bild kann kein Verlaufsgitter werden: Betten Sie es zuerst ein
|
||||
@msg the image has no usable size or position das Bild hat keine verwendbare Größe oder Position
|
||||
@msg the image's pixels can't be read die Pixel des Bildes können nicht gelesen werden
|
||||
Persistent Dauerhaft
|
||||
Library Panel Options Optionen für das Bibliotheksbedienfeld
|
||||
@msg Persistent libraries not found: {files} Dauerhafte Bibliotheken nicht gefunden: {files}
|
||||
@msg `{given}` isn't a library panel (see window.swatchLibrary) `{given}` ist kein Bibliotheksbedienfeld (siehe window.swatchLibrary)
|
||||
@msg library panel `{given}` isn't open Bibliotheksbedienfeld `{given}` ist nicht geöffnet
|
||||
@msg too many library panels were opened in this session in dieser Sitzung wurden zu viele Bibliotheksbedienfelder geöffnet
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5101,3 +5101,9 @@ effect Points Σημεία
|
||||
@msg a linked image can't become a gradient mesh: embed it first μια συνδεδεμένη εικόνα δεν μπορεί να γίνει πλέγμα διαβάθμισης: ενσωματώστε την πρώτα
|
||||
@msg the image has no usable size or position η εικόνα δεν έχει χρησιμοποιήσιμο μέγεθος ή θέση
|
||||
@msg the image's pixels can't be read τα εικονοστοιχεία της εικόνας δεν μπορούν να διαβαστούν
|
||||
Persistent Μόνιμη
|
||||
Library Panel Options Επιλογές πίνακα βιβλιοθήκης
|
||||
@msg Persistent libraries not found: {files} Δεν βρέθηκαν μόνιμες βιβλιοθήκες: {files}
|
||||
@msg `{given}` isn't a library panel (see window.swatchLibrary) Το `{given}` δεν είναι πίνακας βιβλιοθήκης (δείτε window.swatchLibrary)
|
||||
@msg library panel `{given}` isn't open ο πίνακας βιβλιοθήκης `{given}` δεν είναι ανοιχτός
|
||||
@msg too many library panels were opened in this session άνοιξαν πάρα πολλοί πίνακες βιβλιοθηκών σε αυτή τη συνεδρία
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 1993 and column 19.
|
@@ -5091,3 +5091,9 @@ mark-style Japanese Japonesas
|
||||
@msg a linked image can't become a gradient mesh: embed it first una imagen enlazada no puede convertirse en una malla de degradado: incrústela primero
|
||||
@msg the image has no usable size or position la imagen no tiene un tamaño ni una posición utilizables
|
||||
@msg the image's pixels can't be read no se pueden leer los píxeles de la imagen
|
||||
Persistent Persistente
|
||||
Library Panel Options Opciones del panel de biblioteca
|
||||
@msg Persistent libraries not found: {files} No se encontraron bibliotecas persistentes: {files}
|
||||
@msg `{given}` isn't a library panel (see window.swatchLibrary) `{given}` no es un panel de biblioteca (consulte window.swatchLibrary)
|
||||
@msg library panel `{given}` isn't open el panel de biblioteca `{given}` no está abierto
|
||||
@msg too many library panels were opened in this session se abrieron demasiados paneles de biblioteca en esta sesión
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5113,3 +5113,9 @@ mark-style Japanese Japanilainen
|
||||
@msg a linked image can't become a gradient mesh: embed it first linkitetystä kuvasta ei voi tehdä liukuväriverkkoa: upota se ensin
|
||||
@msg the image has no usable size or position kuvalla ei ole käyttökelpoista kokoa tai sijaintia
|
||||
@msg the image's pixels can't be read kuvan pikseleitä ei voi lukea
|
||||
Persistent Pysyvä
|
||||
Library Panel Options Kirjastopaneelin asetukset
|
||||
@msg Persistent libraries not found: {files} Pysyviä kirjastoja ei löytynyt: {files}
|
||||
@msg `{given}` isn't a library panel (see window.swatchLibrary) `{given}` ei ole kirjastopaneeli (katso window.swatchLibrary)
|
||||
@msg library panel `{given}` isn't open kirjastopaneeli `{given}` ei ole auki
|
||||
@msg too many library panels were opened in this session tässä istunnossa on avattu liian monta kirjastopaneelia
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5148,3 +5148,9 @@ theme Appearance Apparence
|
||||
@msg a linked image can't become a gradient mesh: embed it first une image liée ne peut pas devenir un filet de dégradé : incorporez-la d’abord
|
||||
@msg the image has no usable size or position l’image n’a ni taille ni position utilisables
|
||||
@msg the image's pixels can't be read impossible de lire les pixels de l’image
|
||||
Persistent Persistante
|
||||
Library Panel Options Options du panneau de bibliothèque
|
||||
@msg Persistent libraries not found: {files} Bibliothèques persistantes introuvables : {files}
|
||||
@msg `{given}` isn't a library panel (see window.swatchLibrary) `{given}` n’est pas un panneau de bibliothèque (voir window.swatchLibrary)
|
||||
@msg library panel `{given}` isn't open le panneau de bibliothèque `{given}` n’est pas ouvert
|
||||
@msg too many library panels were opened in this session trop de panneaux de bibliothèque ont été ouverts dans cette session
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5135,3 +5135,9 @@ imageTrace Outline Körvonal
|
||||
@msg a linked image can't become a gradient mesh: embed it first csatolt kép nem alakítható színátmenetes hálóvá: előbb ágyazza be
|
||||
@msg the image has no usable size or position a képnek nincs használható mérete vagy helyzete
|
||||
@msg the image's pixels can't be read a kép képpontjai nem olvashatók
|
||||
Persistent Állandó
|
||||
Library Panel Options Könyvtárpanel beállításai
|
||||
@msg Persistent libraries not found: {files} Nem találhatók állandó könyvtárak: {files}
|
||||
@msg `{given}` isn't a library panel (see window.swatchLibrary) A(z) `{given}` nem könyvtárpanel (lásd: window.swatchLibrary)
|
||||
@msg library panel `{given}` isn't open a(z) `{given}` könyvtárpanel nincs megnyitva
|
||||
@msg too many library panels were opened in this session ebben a munkamenetben túl sok könyvtárpanel nyílt meg
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5149,3 +5149,5 @@ swatch Bright Violet Violet cerah
|
||||
Text Orientation Orientasi teks
|
||||
Thread Text Untai teks
|
||||
Typing Mengetik
|
||||
Persistent Persisten
|
||||
Library Panel Options Opsi panel pustaka
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 3755 and column 19.
|
@@ -4762,3 +4762,9 @@ lighting Light Luce
|
||||
@msg a linked image can't become a gradient mesh: embed it first un’immagine collegata non può diventare una trama sfumata: incorporala prima
|
||||
@msg the image has no usable size or position l’immagine non ha dimensioni o posizione utilizzabili
|
||||
@msg the image's pixels can't be read impossibile leggere i pixel dell’immagine
|
||||
Persistent Persistente
|
||||
Library Panel Options Opzioni pannello libreria
|
||||
@msg Persistent libraries not found: {files} Librerie persistenti non trovate: {files}
|
||||
@msg `{given}` isn't a library panel (see window.swatchLibrary) `{given}` non è un pannello libreria (vedi window.swatchLibrary)
|
||||
@msg library panel `{given}` isn't open il pannello libreria `{given}` non è aperto
|
||||
@msg too many library panels were opened in this session in questa sessione sono stati aperti troppi pannelli libreria
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4803,3 +4803,9 @@ lighting Light ライト
|
||||
@msg a linked image can't become a gradient mesh: embed it first リンクされた画像はグラデーションメッシュにできません: 先に埋め込んでください
|
||||
@msg the image has no usable size or position 画像に使用できるサイズまたは位置がありません
|
||||
@msg the image's pixels can't be read 画像のピクセルを読み取れません
|
||||
Persistent 永続
|
||||
Library Panel Options ライブラリパネルオプション
|
||||
@msg Persistent libraries not found: {files} 永続ライブラリが見つかりません: {files}
|
||||
@msg `{given}` isn't a library panel (see window.swatchLibrary) `{given}` はライブラリパネルではありません (window.swatchLibrary を参照)
|
||||
@msg library panel `{given}` isn't open ライブラリパネル `{given}` は開いていません
|
||||
@msg too many library panels were opened in this session このセッションで開いたライブラリパネルが多すぎます
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4726,3 +4726,9 @@ lighting Light 광원
|
||||
@msg a linked image can't become a gradient mesh: embed it first 연결된 이미지는 그라디언트 메시로 만들 수 없습니다: 먼저 포함하십시오
|
||||
@msg the image has no usable size or position 이미지에 사용할 수 있는 크기나 위치가 없습니다
|
||||
@msg the image's pixels can't be read 이미지의 픽셀을 읽을 수 없습니다
|
||||
Persistent 지속
|
||||
Library Panel Options 라이브러리 패널 옵션
|
||||
@msg Persistent libraries not found: {files} 지속 라이브러리를 찾을 수 없습니다: {files}
|
||||
@msg `{given}` isn't a library panel (see window.swatchLibrary) `{given}`은(는) 라이브러리 패널이 아닙니다(window.swatchLibrary 참조)
|
||||
@msg library panel `{given}` isn't open 라이브러리 패널 `{given}`이(가) 열려 있지 않습니다
|
||||
@msg too many library panels were opened in this session 이 세션에서 라이브러리 패널을 너무 많이 열었습니다
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5149,3 +5149,5 @@ swatch Bright Violet Felviolet
|
||||
Text Orientation Tekstrichting
|
||||
Thread Text Tekst koppelen
|
||||
Typing Typen
|
||||
Persistent Permanent
|
||||
Library Panel Options Opties voor bibliotheekpaneel
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5001,3 +5001,9 @@ mark-style Japanese Japońskie
|
||||
@msg a linked image can't become a gradient mesh: embed it first połączony obraz nie może stać się siatką gradientu: najpierw go osadź
|
||||
@msg the image has no usable size or position obraz nie ma użytecznego rozmiaru ani położenia
|
||||
@msg the image's pixels can't be read nie można odczytać pikseli obrazu
|
||||
Persistent Trwały
|
||||
Library Panel Options Opcje panelu biblioteki
|
||||
@msg Persistent libraries not found: {files} Nie znaleziono trwałych bibliotek: {files}
|
||||
@msg `{given}` isn't a library panel (see window.swatchLibrary) `{given}` nie jest panelem biblioteki (zobacz window.swatchLibrary)
|
||||
@msg library panel `{given}` isn't open panel biblioteki `{given}` nie jest otwarty
|
||||
@msg too many library panels were opened in this session w tej sesji otwarto zbyt wiele paneli bibliotek
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5066,3 +5066,9 @@ mark-style Japanese Japonesas
|
||||
@msg a linked image can't become a gradient mesh: embed it first uma imagem vinculada não pode virar uma malha de degradê: incorpore-a primeiro
|
||||
@msg the image has no usable size or position a imagem não tem tamanho nem posição utilizáveis
|
||||
@msg the image's pixels can't be read não é possível ler os pixels da imagem
|
||||
Persistent Persistente
|
||||
Library Panel Options Opções do Painel de Biblioteca
|
||||
@msg Persistent libraries not found: {files} Bibliotecas persistentes não encontradas: {files}
|
||||
@msg `{given}` isn't a library panel (see window.swatchLibrary) `{given}` não é um painel de biblioteca (consulte window.swatchLibrary)
|
||||
@msg library panel `{given}` isn't open o painel de biblioteca `{given}` não está aberto
|
||||
@msg too many library panels were opened in this session foram abertos painéis de biblioteca demais nesta sessão
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -4758,3 +4758,9 @@ lighting Light Свет
|
||||
@msg a linked image can't become a gradient mesh: embed it first связанное изображение нельзя превратить в сетчатый градиент: сначала встройте его
|
||||
@msg the image has no usable size or position у изображения нет пригодного размера или положения
|
||||
@msg the image's pixels can't be read не удаётся прочитать пиксели изображения
|
||||
Persistent Постоянная
|
||||
Library Panel Options Параметры панели библиотеки
|
||||
@msg Persistent libraries not found: {files} Постоянные библиотеки не найдены: {files}
|
||||
@msg `{given}` isn't a library panel (see window.swatchLibrary) `{given}` — не панель библиотеки (см. window.swatchLibrary)
|
||||
@msg library panel `{given}` isn't open панель библиотеки `{given}` не открыта
|
||||
@msg too many library panels were opened in this session в этом сеансе открыто слишком много панелей библиотек
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5105,3 +5105,9 @@ theme Appearance Utseende
|
||||
@msg a linked image can't become a gradient mesh: embed it first en länkad bild kan inte bli ett övertoningsnät: bädda in den först
|
||||
@msg the image has no usable size or position bilden har ingen användbar storlek eller position
|
||||
@msg the image's pixels can't be read bildens pixlar kan inte läsas
|
||||
Persistent Beständig
|
||||
Library Panel Options Alternativ för bibliotekspanelen
|
||||
@msg Persistent libraries not found: {files} Beständiga bibliotek hittades inte: {files}
|
||||
@msg `{given}` isn't a library panel (see window.swatchLibrary) `{given}` är inte en bibliotekspanel (se window.swatchLibrary)
|
||||
@msg library panel `{given}` isn't open bibliotekspanelen `{given}` är inte öppen
|
||||
@msg too many library panels were opened in this session för många bibliotekspaneler har öppnats i den här sessionen
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -4742,3 +4742,9 @@ lighting Light Світло
|
||||
@msg a linked image can't become a gradient mesh: embed it first зв’язане зображення не можна перетворити на сітчастий градієнт: спочатку вбудуйте його
|
||||
@msg the image has no usable size or position зображення не має придатного розміру чи положення
|
||||
@msg the image's pixels can't be read не вдається прочитати пікселі зображення
|
||||
Persistent Постійна
|
||||
Library Panel Options Параметри панелі бібліотеки
|
||||
@msg Persistent libraries not found: {files} Постійні бібліотеки не знайдено: {files}
|
||||
@msg `{given}` isn't a library panel (see window.swatchLibrary) `{given}` — не панель бібліотеки (див. window.swatchLibrary)
|
||||
@msg library panel `{given}` isn't open панель бібліотеки `{given}` не відкрита
|
||||
@msg too many library panels were opened in this session у цьому сеансі відкрито забагато панелей бібліотек
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 723 and column 40.
|
@@ -4742,3 +4742,9 @@ theme Appearance Giao diện
|
||||
@msg a linked image can't become a gradient mesh: embed it first không thể biến ảnh liên kết thành lưới chuyển sắc: hãy nhúng ảnh trước
|
||||
@msg the image has no usable size or position ảnh không có kích thước hoặc vị trí dùng được
|
||||
@msg the image's pixels can't be read không đọc được các điểm ảnh của ảnh
|
||||
Persistent Lưu giữ
|
||||
Library Panel Options Tùy chọn bảng thư viện
|
||||
@msg Persistent libraries not found: {files} Không tìm thấy thư viện lưu giữ: {files}
|
||||
@msg `{given}` isn't a library panel (see window.swatchLibrary) `{given}` không phải là bảng thư viện (xem window.swatchLibrary)
|
||||
@msg library panel `{given}` isn't open bảng thư viện `{given}` chưa được mở
|
||||
@msg too many library panels were opened in this session đã mở quá nhiều bảng thư viện trong phiên này
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 3936 and column 10.
|
@@ -4985,3 +4985,9 @@ lighting Light 光源
|
||||
@msg a linked image can't become a gradient mesh: embed it first 链接的图像无法转换为渐变网格:请先嵌入它
|
||||
@msg the image has no usable size or position 图像没有可用的尺寸或位置
|
||||
@msg the image's pixels can't be read 无法读取图像的像素
|
||||
Persistent 持久
|
||||
Library Panel Options 库面板选项
|
||||
@msg Persistent libraries not found: {files} 找不到持久库:{files}
|
||||
@msg `{given}` isn't a library panel (see window.swatchLibrary) `{given}` 不是库面板 (请参阅 window.swatchLibrary)
|
||||
@msg library panel `{given}` isn't open 库面板 `{given}` 未打开
|
||||
@msg too many library panels were opened in this session 本次会话中打开的库面板过多
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4058,3 +4058,5 @@ lighting Light 光源
|
||||
@msg name is empty 名稱為空
|
||||
@msg no symbol named `{n}` 沒有名為 `{n}` 的符號
|
||||
Enable Guides for 9-Slice Scaling 啟用 9-slice 縮放參考線
|
||||
Persistent 持久
|
||||
Library Panel Options 資料庫面板選項
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -1387,6 +1387,7 @@ impl VectorcraftApp {
|
||||
if self.ui.toolbar && self.ui.screen_mode < 3 {
|
||||
toolbar::show(self, ui);
|
||||
}
|
||||
panels::library_panel::tidy(self);
|
||||
if self.ui.dock && self.ui.screen_mode < 3 {
|
||||
dock::show(self, ui);
|
||||
}
|
||||
@@ -1398,7 +1399,6 @@ impl VectorcraftApp {
|
||||
});
|
||||
dock::floating_panel(self, &ctx);
|
||||
floating::show(self, &ctx);
|
||||
panels::library_panel::show_window(self, &ctx);
|
||||
dialogs::show(self, &ctx);
|
||||
palette::show(self, &ctx);
|
||||
if self.custom_titlebar {
|
||||
|
||||
@@ -495,13 +495,19 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
"window.swatchLibrary",
|
||||
"Swatch Library",
|
||||
"",
|
||||
"{library: id or name (see swatch.library.list) | null (close)} open the read-only library panel on a swatch library (UI state `library_panel`); clicking a swatch there runs swatch.library.add with apply → {open, name, count}",
|
||||
"{library: id or name (see swatch.library.list) | null (close every swatch library panel)} open the swatch library in a read-only panel of its own, floating (panel id `library:swatches:<id>`, which window.panel.* moves, docks and closes), or bring its panel forward when it is open; clicking a swatch there runs swatch.library.add with apply. ui.libraryPanel sets its view, find field and Persistent (ui.inspect: ui.library_panels) → {open: id, panel, name, count}",
|
||||
),
|
||||
(
|
||||
"window.swatchLibrary.other",
|
||||
"Other Library…",
|
||||
"",
|
||||
"{path?} (default: pick a file) load a .vcswatches, .gpl, .ase or .acb (color book) library, or another document's swatches (engine: swatch.library.load), and open it in the library panel",
|
||||
"{path?} (default: pick a file) load a .vcswatches, .gpl, .ase or .acb (color book) library, or another document's swatches (engine: swatch.library.load), and open it in its library panel",
|
||||
),
|
||||
(
|
||||
"ui.libraryPanel",
|
||||
"Library Panel Options",
|
||||
"",
|
||||
"{panel: a library panel's id (`library:swatches:pastels`, as window.swatchLibrary answers), library?: id or name of another library of its kind to show in the panel (Previous / Next Library; one open in a panel of its own is brought forward instead), view?: smallThumb|mediumThumb|largeThumb|smallList|largeList, showFind?: bool, persistent?: bool (reopen the panel when the app starts)} set an open library panel, or a closed persistent one's view, Show Find Field and Persistent; kept with the preferences (ui.inspect: ui.library_panels) → {panel, kind, library, view, showFind, persistent, open}",
|
||||
),
|
||||
(
|
||||
"ui.saveSwatchLibrary",
|
||||
@@ -589,13 +595,13 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
"window.graphicStyleLibrary",
|
||||
"Graphic Style Library",
|
||||
"",
|
||||
"{library: id or name (see graphicStyle.libraries) | null (close)} open the read-only library panel on a graphic style library (UI state `library_panel`, kind graphicStyles); clicking a style there runs graphicStyle.addFromLibrary with apply (Alt: add) → {open, name, count}",
|
||||
"{library: id or name (see graphicStyle.libraries) | null (close every graphic style library panel)} open the graphic style library in a read-only panel of its own, floating (panel id `library:graphicStyles:<id>`), or bring its panel forward when it is open; clicking a style there runs graphicStyle.addFromLibrary with apply (Alt: add). ui.libraryPanel sets its view, find field and Persistent → {open: id, panel, name, count}",
|
||||
),
|
||||
(
|
||||
"window.graphicStyleLibrary.other",
|
||||
"Other Library…",
|
||||
"",
|
||||
"{path?} (default: pick a file) load a .vcstyles library, or another document's graphic styles (engine: graphicStyle.loadLibrary), and open it in the library panel",
|
||||
"{path?} (default: pick a file) load a .vcstyles library, or another document's graphic styles (engine: graphicStyle.loadLibrary), and open it in its library panel",
|
||||
),
|
||||
(
|
||||
"ui.saveGraphicStyleLibrary",
|
||||
@@ -992,10 +998,14 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
];
|
||||
|
||||
/// Canonical panel id for `window.panel`: a dock tab's or an icon panel's id or display label,
|
||||
/// matched case-insensitively (`"Layers"`, `"swatches"`, `"Color Guide"`).
|
||||
/// matched case-insensitively (`"Layers"`, `"swatches"`, `"Color Guide"`), or a library panel's id
|
||||
/// as it is (`library:swatches:pastels`).
|
||||
pub(crate) fn normalize_panel(input: &str) -> Option<&'static str> {
|
||||
let name = input.trim();
|
||||
crate::state::all_panels().find(|(id, label)| name.eq_ignore_ascii_case(id) || name.eq_ignore_ascii_case(label)).map(|(id, _)| id)
|
||||
crate::state::all_panels()
|
||||
.find(|(id, label)| name.eq_ignore_ascii_case(id) || name.eq_ignore_ascii_case(label))
|
||||
.map(|(id, _)| id)
|
||||
.or_else(|| crate::panels::library_panel::intern(input))
|
||||
}
|
||||
|
||||
/// An optional `{key?: bool}` param: none when it is omitted or null (the commands then toggle).
|
||||
@@ -1405,6 +1415,8 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<R
|
||||
app.ui.dock_tab = tab;
|
||||
Ok(Value::Null)
|
||||
}
|
||||
// A library panel isn't in the icon column: it floats.
|
||||
(Some(p), None) if crate::panels::library_panel::parse(p).is_some() => crate::panels::library_panel::float(app, p),
|
||||
(Some(p), None) => {
|
||||
app.ui.open_panel = if app.ui.open_panel.as_deref() == Some(p) { None } else { Some(p.to_string()) };
|
||||
app.ui.dock = true;
|
||||
@@ -1538,6 +1550,7 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<R
|
||||
}
|
||||
"ui.flattenerPreview" => crate::panels::flattener_preview::command(app, p),
|
||||
"window.graphicStyleLibrary" => crate::panels::graphic_styles::open_library(app, p),
|
||||
"ui.libraryPanel" => crate::panels::library_panel::command(app, p),
|
||||
"window.graphicStyleLibrary.other" => crate::panels::graphic_styles::other_library(app, s("path")),
|
||||
"ui.saveGraphicStyleLibrary" => {
|
||||
let names = p.get("names").and_then(Value::as_array).map(|a| a.iter().filter_map(Value::as_str).map(str::to_string).collect());
|
||||
|
||||
@@ -625,6 +625,13 @@ pub(crate) fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) -> bool {
|
||||
display
|
||||
.floating
|
||||
.retain(|group| !app.ui.floating_panels.iter().any(|native| decorated(native) && native.panels.iter().any(|id| group.panels.contains(id))));
|
||||
// Library panels are titled by their libraries' names.
|
||||
let library_labels: Vec<(String, String)> = layout
|
||||
.panels()
|
||||
.into_iter()
|
||||
.filter_map(|panel| crate::panels::library_panel::tab_label(app, panel).map(|label| (panel.clone(), label)))
|
||||
.collect();
|
||||
let library_label = |panel: &str| library_labels.iter().find(|(p, _)| p == panel).map(|(_, label)| label.clone());
|
||||
// Floating groups lie on the canvas, not on the dock's card: reserve a fill under each one
|
||||
// before the dock draws it, sized once it is placed, so the canvas doesn't show through (#2421).
|
||||
let fills: Vec<_> = display
|
||||
@@ -646,8 +653,16 @@ pub(crate) fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) -> bool {
|
||||
let info = crate::state::DockTab::from_id(panel)
|
||||
.map(|tab| tab.info())
|
||||
.or_else(|| crate::state::ICON_PANELS.iter().find(|entry| entry.0 == panel).copied());
|
||||
if let Some((_, label, icon)) = info
|
||||
&& crate::widgets::icon_button(ui, icon, tl!(label), app.ui.open_panel.as_ref() == Some(panel), 30.0).clicked()
|
||||
let label = info.map(|(_, label, _)| tl!(label).to_string()).or_else(|| library_label(panel));
|
||||
if let Some(label) = label
|
||||
&& crate::widgets::icon_button(
|
||||
ui,
|
||||
info.map_or("library", |(_, _, icon)| icon),
|
||||
&label,
|
||||
app.ui.open_panel.as_ref() == Some(panel),
|
||||
30.0,
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
app.ui.open_panel = if app.ui.open_panel.as_ref() == Some(panel) { None } else { Some(panel.clone()) };
|
||||
}
|
||||
@@ -675,7 +690,13 @@ pub(crate) fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) -> bool {
|
||||
ui,
|
||||
&display,
|
||||
&style,
|
||||
|id| crate::state::all_panels().find(|p| p.0 == id).map(|p| tl!(p.1).to_string()).unwrap_or_else(|| id.clone()),
|
||||
|id| {
|
||||
crate::state::all_panels()
|
||||
.find(|p| p.0 == id)
|
||||
.map(|p| tl!(p.1).to_string())
|
||||
.or_else(|| library_label(id))
|
||||
.unwrap_or_else(|| id.clone())
|
||||
},
|
||||
|_| craft_ui::docking::Permissions::default(),
|
||||
|_| craft_ui::docking::PanelLimits { min: egui::vec2(220.0, 80.0), ..Default::default() },
|
||||
&mut content,
|
||||
|
||||
@@ -640,3 +640,35 @@ fn floating_group_paints_an_opaque_background() {
|
||||
assert!(opaque_at(pos2(690.0, 210.0)), "tab strip is transparent");
|
||||
assert!(opaque_at(pos2(690.0, 610.0)), "body is transparent");
|
||||
}
|
||||
|
||||
/// Library panels (#2603) float in the shared layout titled by their libraries' names, and their
|
||||
/// menu's Persistent item keeps them for the next launch.
|
||||
#[test]
|
||||
fn library_panels_float_with_their_names_and_a_persistent_menu_item() {
|
||||
let mut app = app();
|
||||
app.run("file.new", json!({})).unwrap();
|
||||
for (library, x) in [("pastels", 40), ("brights", 420)] {
|
||||
let panel = format!("library:swatches:{library}");
|
||||
app.run("window.swatchLibrary", json!({"library": library})).unwrap();
|
||||
app.run("window.panel.float", json!({"panel": panel, "x": x, "y": 100})).unwrap();
|
||||
}
|
||||
let mut h = harness(app, vec2(1280.0, 900.0));
|
||||
for name in ["Pastels", "Brights"] {
|
||||
assert!(h.query_all_by_label(name).next().is_some(), "the {name} tab");
|
||||
}
|
||||
let click = |h: &mut Harness<'static, VectorcraftApp>, at: Pos2| {
|
||||
h.hover_at(at);
|
||||
h.run_steps(1);
|
||||
for pressed in [true, false] {
|
||||
h.event(Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE });
|
||||
h.run_steps(1);
|
||||
}
|
||||
h.run_steps(2);
|
||||
};
|
||||
let menu = h.query_all_by_label("Panel menu").find(|node| node.rect().center().x < 400.0).expect("the Pastels panel's menu").rect().center();
|
||||
click(&mut h, menu);
|
||||
let item = h.query_all_by_label(" Persistent").next().expect("Persistent is in the panel menu").rect().center();
|
||||
click(&mut h, item);
|
||||
let persistent: Vec<&str> = h.state().ui.library_panels.iter().filter(|p| p.persistent).map(|p| p.id.as_str()).collect();
|
||||
assert_eq!(persistent, ["pastels"]);
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ fn limit_menu(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
}
|
||||
}
|
||||
ui.separator();
|
||||
chosen = library_panel::library_items(ui, &colour_libraries(app), Some(&cur)).or(chosen);
|
||||
chosen = library_panel::library_items(ui, &colour_libraries(app), &|id| id == cur).or(chosen);
|
||||
if let Some(key) = chosen {
|
||||
set_limit(app, &json!({ "library": key })).ok();
|
||||
clear_selection(ui.ctx());
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
//! to the art it is dropped on (`canvas::panel_drop`); art dragged off the canvas or the Appearance
|
||||
//! panel's thumbnail dropped on the panel becomes a new style. Holding the right button on a style
|
||||
//! shows it large, on the selected object when there is one. The Control bar's Style picker lists
|
||||
//! the same tiles ([`picker`]). Graphic style libraries open in the library panel
|
||||
//! the same tiles ([`picker`]). Graphic style libraries open in library panels
|
||||
//! ([`GraphicStyleLibraries`]).
|
||||
|
||||
use std::cell::RefCell;
|
||||
@@ -479,7 +479,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let has_sel = selection_len(app) > 0;
|
||||
let linked = app.session.selection_graphic_style().is_some();
|
||||
widgets::bottom_bar(ui, |ui| {
|
||||
let open = app.ui.library_panel.as_ref().is_some_and(|o| o.kind == GraphicStyleLibraries::KIND);
|
||||
let open = library_panel::any_open::<GraphicStyleLibraries>(&app.ui);
|
||||
let lr = widgets::icon_button(ui, "library", GraphicStyleLibraries::MENU, open, 24.0);
|
||||
egui::Popup::menu(&lr).show(|ui| {
|
||||
ui.set_min_width(200.0);
|
||||
@@ -681,13 +681,15 @@ fn describe(g: &GraphicStyle) -> String {
|
||||
parts.join(", ")
|
||||
}
|
||||
|
||||
/// Graphic style libraries in the library panel (engine: `graphicStyle.libraries`,
|
||||
/// Graphic style libraries in library panels (engine: `graphicStyle.libraries`,
|
||||
/// `graphicStyle.addFromLibrary`…).
|
||||
pub(crate) struct GraphicStyleLibraries;
|
||||
|
||||
impl LibraryKind for GraphicStyleLibraries {
|
||||
const KIND: &'static str = "graphicStyles";
|
||||
const OPEN: &'static str = "window.graphicStyleLibrary";
|
||||
const LIST: &'static str = "graphicStyle.libraries";
|
||||
const LOAD: &'static str = "graphicStyle.loadLibrary";
|
||||
const MENU: &'static str = "Graphic Style Libraries Menu";
|
||||
const ADD: &'static str = "Add to Graphic Styles";
|
||||
/// Styles need room to show their effects.
|
||||
@@ -702,6 +704,12 @@ impl LibraryKind for GraphicStyleLibraries {
|
||||
fn get(app: &VectorcraftApp, id: &str) -> Option<(String, Self::Lib)> {
|
||||
stylelib::library(&app.session, id).map(|(info, lib)| (info.name, shown_library(lib)))
|
||||
}
|
||||
fn find(app: &VectorcraftApp, key: &str) -> Option<(String, String, usize)> {
|
||||
stylelib::library(&app.session, key).map(|(info, lib)| (info.id, info.name, lib.len()))
|
||||
}
|
||||
fn path(app: &VectorcraftApp, id: &str) -> Option<String> {
|
||||
app.session.style_libraries.path(id).map(str::to_string)
|
||||
}
|
||||
fn rows<'a>(lib: &'a Self::Lib, query: &str) -> Vec<Row<'a, LibStyle>> {
|
||||
let found = |n: &str| query.is_empty() || n.to_lowercase().contains(query);
|
||||
lib.iter().filter(|s| found(&s.style().name)).map(|s| Row { name: &s.style().name, item: Some(s) }).collect()
|
||||
@@ -740,15 +748,13 @@ impl LibraryKind for GraphicStyleLibraries {
|
||||
}
|
||||
}
|
||||
|
||||
/// `window.graphicStyleLibrary {library}`: open a graphic style library in the library panel
|
||||
/// (`library` null closes it).
|
||||
/// `window.graphicStyleLibrary {library}`: open a graphic style library in its own panel
|
||||
/// (`library` null closes every graphic style library panel).
|
||||
pub(crate) fn open_library(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
|
||||
library_panel::open_command::<GraphicStyleLibraries>(app, p, "graphicStyle.libraries", |app, key| {
|
||||
stylelib::library(&app.session, key).map(|(info, lib)| (info.id, info.name, lib.len()))
|
||||
})
|
||||
library_panel::open_command::<GraphicStyleLibraries>(app, p)
|
||||
}
|
||||
|
||||
/// `graphicStyle.loadLibrary` params, then open the library in the panel.
|
||||
/// `graphicStyle.loadLibrary` 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("graphicStyle.loadLibrary", params)?;
|
||||
open_library(app, &json!({ "library": r["library"] }))
|
||||
|
||||
@@ -53,7 +53,7 @@ fn library_label(app: &VectorcraftApp, key: &str) -> String {
|
||||
fn library_list(app: &VectorcraftApp, ui: &mut Ui, current: &str) -> Option<String> {
|
||||
let document = menu_item(ui, swatches::DOCUMENT_SWATCHES, true, current == DOCUMENT_SWATCHES).then(|| DOCUMENT_SWATCHES.to_string());
|
||||
ui.separator();
|
||||
library_panel::library_items(ui, &swatches::colour_libraries(app), Some(current)).or(document)
|
||||
library_panel::library_items(ui, &swatches::colour_libraries(app), &|id| id == current).or(document)
|
||||
}
|
||||
|
||||
/// Panel state: the preset name, the current parameters, the view and the last result's counts.
|
||||
|
||||
@@ -1,30 +1,62 @@
|
||||
//! The library panel: a floating, read-only view of one library (swatch and graphic style
|
||||
//! libraries; any kind of library implementing [`LibraryKind`]) with thumbnail and list views, a
|
||||
//! find field, the libraries menu and previous / next arrows. A plain click on an item adds it to
|
||||
//! the document and applies it; Shift and Cmd/Ctrl-clicks select items (and folders) for Add to ….
|
||||
//! Library panels: read-only views of one library each (swatch and graphic style libraries; any
|
||||
//! kind of library implementing [`LibraryKind`]) with thumbnail and list views, a find field, the
|
||||
//! libraries menu and previous / next arrows. A plain click on an item adds it to the document and
|
||||
//! applies it; Shift and Cmd/Ctrl-clicks select items (and folders) for Add to ….
|
||||
//!
|
||||
//! Which library is open lives in `UiState::library_panel` (`window.swatchLibrary` and
|
||||
//! `window.graphicStyleLibrary` open one), so agents can open and read it; the view, find text and
|
||||
//! selection are panel state.
|
||||
//! As in Illustrator (<https://helpx.adobe.com/illustrator/using/using-creating-swatches.html>,
|
||||
//! "Swatch libraries"), each open library is a panel of its own, `library:<kind>:<library id>`
|
||||
//! ([`panel_id`]), that floats, tabs, docks and closes as any panel of the shared docking system:
|
||||
//! `window.swatchLibrary` and `window.graphicStyleLibrary` open one, or bring it forward when it is
|
||||
//! open. `UiState::library_panels` keeps each one's view, Show Find Field and Persistent
|
||||
//! (`ui.libraryPanel`); a persistent panel reopens when the app starts ([`restore`]). The find text
|
||||
//! and the selection are panel state.
|
||||
|
||||
use egui::{CornerRadius, Rect, Sense, Ui, vec2};
|
||||
use std::sync::{Mutex, PoisonError};
|
||||
|
||||
use egui::{Rect, Sense, Ui};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_engine::cmd::swatchlib::LibraryInfo;
|
||||
|
||||
use super::swatches::{View, click_selection, folder_tile, list_row, list_row_name, tile_grid};
|
||||
use super::{pstate, set_pstate};
|
||||
use crate::theme::{self, Tokens};
|
||||
use crate::VectorcraftApp;
|
||||
use crate::state::UiState;
|
||||
use crate::widgets::{self, menu_item};
|
||||
use crate::{VectorcraftApp, icons};
|
||||
|
||||
/// The library open in the library panel.
|
||||
/// What every library panel's id starts with.
|
||||
pub const PREFIX: &str = "library:";
|
||||
|
||||
/// The most library panels one session opens: their ids are interned ([`intern`]).
|
||||
const MAX_PANELS: usize = 256;
|
||||
|
||||
/// A library panel's settings: one per library opened this session, and the persistent ones
|
||||
/// (also once closed), most recently opened or brought forward last.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct OpenLibrary {
|
||||
pub struct LibraryPanel {
|
||||
/// The kind of library ([`LibraryKind::KIND`], e.g. "swatches").
|
||||
pub kind: String,
|
||||
/// The library's id.
|
||||
pub id: String,
|
||||
/// The file a loaded library was read from, which [`restore`] reads it from again.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub path: Option<String>,
|
||||
/// The view picked from the panel menu (none: the kind's, [`LibraryKind::VIEW`]).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub view: Option<View>,
|
||||
/// Show Find Field is off.
|
||||
#[serde(default)]
|
||||
pub hide_find: bool,
|
||||
/// Persistent: the panel reopens when the app starts.
|
||||
#[serde(default)]
|
||||
pub persistent: bool,
|
||||
}
|
||||
|
||||
impl LibraryPanel {
|
||||
/// The panel's id.
|
||||
pub fn panel(&self) -> String {
|
||||
panel_id(&self.kind, &self.id)
|
||||
}
|
||||
}
|
||||
|
||||
/// A library as the libraries menu lists it.
|
||||
@@ -41,17 +73,21 @@ pub(crate) struct Row<'a, I> {
|
||||
pub item: Option<&'a I>,
|
||||
}
|
||||
|
||||
/// One kind of library the panel shows.
|
||||
/// One kind of library the panels show.
|
||||
pub(crate) trait LibraryKind {
|
||||
/// `OpenLibrary::kind`, and the prefix of the panel's state keys.
|
||||
/// `LibraryPanel::kind`, and the middle part of its panels' ids.
|
||||
const KIND: &'static str;
|
||||
/// The UI command that opens a library in the panel (`{library: id}`).
|
||||
/// The UI command that opens a library's panel (`{library: id}`).
|
||||
const OPEN: &'static str;
|
||||
/// The engine command listing the libraries, for error messages.
|
||||
const LIST: &'static str;
|
||||
/// The engine command loading a library from a file (`{path}` → `{library}`).
|
||||
const LOAD: &'static str;
|
||||
/// The libraries menu's tooltip ("Swatch Libraries Menu").
|
||||
const MENU: &'static str;
|
||||
/// What adding the selection is called ("Add to Swatches").
|
||||
const ADD: &'static str;
|
||||
/// The view the panel shows until another is picked.
|
||||
/// The view a panel shows until another is picked.
|
||||
const VIEW: View = View::MediumThumb;
|
||||
/// A library's contents, cheap to clone.
|
||||
type Lib;
|
||||
@@ -60,6 +96,10 @@ pub(crate) trait LibraryKind {
|
||||
fn list(app: &VectorcraftApp) -> Vec<LibraryRef>;
|
||||
/// Library `id`: its name and contents.
|
||||
fn get(app: &VectorcraftApp, id: &str) -> Option<(String, Self::Lib)>;
|
||||
/// Library `key` (an id or a name).
|
||||
fn find(app: &VectorcraftApp, key: &str) -> Found;
|
||||
/// The file library `id` was read from, if any.
|
||||
fn path(app: &VectorcraftApp, id: &str) -> Option<String>;
|
||||
/// The rows matching the find field's `query` (lower case; empty: all).
|
||||
fn rows<'a>(lib: &'a Self::Lib, query: &str) -> Vec<Row<'a, Self::Item>>;
|
||||
/// An item's thumbnail in `r`.
|
||||
@@ -78,31 +118,236 @@ pub(crate) trait LibraryKind {
|
||||
fn menu_tail(_app: &mut VectorcraftApp, _ui: &mut Ui) {}
|
||||
}
|
||||
|
||||
/// The state key `key` of `K`'s panel.
|
||||
fn key<K: LibraryKind>(key: &str) -> String {
|
||||
format!("{}-library-{key}", K::KIND)
|
||||
/// A library looked up by its id or name: its id, name and item count.
|
||||
pub(crate) type Found = Option<(String, String, usize)>;
|
||||
|
||||
/// What code that knows a library panel by its id alone needs of its kind.
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) struct Kind {
|
||||
pub kind: &'static str,
|
||||
list: &'static str,
|
||||
load: &'static str,
|
||||
view: View,
|
||||
name: fn(&VectorcraftApp, &str) -> Option<String>,
|
||||
find: fn(&VectorcraftApp, &str) -> Found,
|
||||
path: fn(&VectorcraftApp, &str) -> Option<String>,
|
||||
body: fn(&mut VectorcraftApp, &mut Ui, &str),
|
||||
menu: fn(&mut VectorcraftApp, &mut Ui, &str),
|
||||
}
|
||||
|
||||
/// The view of `K`'s panel.
|
||||
fn view<K: LibraryKind>(ctx: &egui::Context) -> View {
|
||||
pstate::<Option<View>>(ctx, &key::<K>("view")).unwrap_or(K::VIEW)
|
||||
}
|
||||
|
||||
/// The interaction id of the tile or row of `name`.
|
||||
pub(crate) fn tile_id<K: LibraryKind>(name: &str) -> egui::Id {
|
||||
egui::Id::new(("library-tile", K::KIND, name))
|
||||
}
|
||||
|
||||
/// Draw the open library panel, if any.
|
||||
pub fn show_window(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
let Some(open) = app.ui.library_panel.clone() else { return };
|
||||
match open.kind.as_str() {
|
||||
super::swatches::SwatchLibraries::KIND => window::<super::swatches::SwatchLibraries>(app, ctx, &open.id),
|
||||
super::graphic_styles::GraphicStyleLibraries::KIND => window::<super::graphic_styles::GraphicStyleLibraries>(app, ctx, &open.id),
|
||||
_ => app.ui.library_panel = None,
|
||||
fn kind_of<K: LibraryKind>() -> Kind {
|
||||
Kind {
|
||||
kind: K::KIND,
|
||||
list: K::LIST,
|
||||
load: K::LOAD,
|
||||
view: K::VIEW,
|
||||
name: |app, id| K::get(app, id).map(|(name, _)| name),
|
||||
find: K::find,
|
||||
path: K::path,
|
||||
body: body::<K>,
|
||||
menu: panel_menu::<K>,
|
||||
}
|
||||
}
|
||||
|
||||
/// The kind of library `kind` names.
|
||||
fn kind(kind: &str) -> Option<Kind> {
|
||||
[kind_of::<super::swatches::SwatchLibraries>(), kind_of::<super::graphic_styles::GraphicStyleLibraries>()].into_iter().find(|k| k.kind == kind)
|
||||
}
|
||||
|
||||
/// The id of the panel of library `id` of `kind` (`library:swatches:pastels`).
|
||||
pub fn panel_id(kind: &str, id: &str) -> String {
|
||||
format!("{PREFIX}{kind}:{id}")
|
||||
}
|
||||
|
||||
/// The kind and library id of library panel `panel`.
|
||||
pub(crate) fn parse(panel: &str) -> Option<(Kind, &str)> {
|
||||
let (k, id) = panel.strip_prefix(PREFIX)?.split_once(':')?;
|
||||
if id.is_empty() || panel.len() > 1024 {
|
||||
return None;
|
||||
}
|
||||
Some((kind(k)?, id))
|
||||
}
|
||||
|
||||
/// Library panel `panel` as the `&'static str` the docking code keeps panel ids as
|
||||
/// ([`crate::menus::normalize_panel`]); `None` when it isn't a library panel's id.
|
||||
/// shortcut: each id is leaked once, at most `MAX_PANELS` of them (more libraries than one session
|
||||
/// opens); make the docking code keep owned ids if that ever binds.
|
||||
pub(crate) fn intern(panel: &str) -> Option<&'static str> {
|
||||
parse(panel)?;
|
||||
static IDS: Mutex<Vec<&'static str>> = Mutex::new(Vec::new());
|
||||
let mut ids = IDS.lock().unwrap_or_else(PoisonError::into_inner);
|
||||
if let Some(id) = ids.iter().find(|id| **id == panel) {
|
||||
return Some(id);
|
||||
}
|
||||
if ids.len() >= MAX_PANELS {
|
||||
return None;
|
||||
}
|
||||
let id: &'static str = Box::leak(panel.into());
|
||||
ids.push(id);
|
||||
Some(id)
|
||||
}
|
||||
|
||||
/// [`intern`], failing as a command does.
|
||||
fn registered(panel: &str) -> Result<&'static str, String> {
|
||||
intern(panel).ok_or_else(|| "too many library panels were opened in this session".into())
|
||||
}
|
||||
|
||||
/// Is panel `panel` open (in the dock or floating)?
|
||||
pub(crate) fn is_open(ui: &UiState, panel: &str) -> bool {
|
||||
ui.docking.as_ref().is_some_and(|l| l.panels().iter().any(|p| p.as_str() == panel)) || crate::floating::group_of(ui, panel).is_some()
|
||||
}
|
||||
|
||||
/// The open library panels.
|
||||
fn open_panels(ui: &UiState) -> Vec<String> {
|
||||
let mut out: Vec<String> = vec![];
|
||||
let docked = ui.docking.iter().flat_map(|l| l.panels());
|
||||
for p in docked.chain(ui.floating_panels.iter().flat_map(|g| &g.panels)) {
|
||||
if p.starts_with(PREFIX) && !out.contains(p) {
|
||||
out.push(p.clone());
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Is a library panel of kind `K` open?
|
||||
pub(crate) fn any_open<K: LibraryKind>(ui: &UiState) -> bool {
|
||||
open_panels(ui).iter().any(|p| parse(p).is_some_and(|(k, _)| k.kind == K::KIND))
|
||||
}
|
||||
|
||||
/// The library panel opened or brought forward last that is open.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn last_open(ui: &UiState) -> Option<&LibraryPanel> {
|
||||
ui.library_panels.iter().rev().find(|p| is_open(ui, &p.panel()))
|
||||
}
|
||||
|
||||
fn settings<'a>(ui: &'a UiState, kind: &str, id: &str) -> Option<&'a LibraryPanel> {
|
||||
ui.library_panels.iter().find(|p| p.kind == kind && p.id == id)
|
||||
}
|
||||
|
||||
/// The settings of library `id`'s panel, made when it has none.
|
||||
fn settings_mut<'a>(app: &'a mut VectorcraftApp, k: Kind, id: &str) -> Option<&'a mut LibraryPanel> {
|
||||
if settings(&app.ui, k.kind, id).is_none() {
|
||||
let path = if id.starts_with("loaded/") { (k.path)(app, id) } else { None };
|
||||
app.ui.library_panels.push(LibraryPanel { kind: k.kind.into(), id: id.into(), path, view: None, hide_find: false, persistent: false });
|
||||
}
|
||||
app.ui.library_panels.iter_mut().find(|p| p.kind == k.kind && p.id == id)
|
||||
}
|
||||
|
||||
/// Library panel `panel`'s tab label: its library's name (a built-in one's in the UI language).
|
||||
pub(crate) fn tab_label(app: &VectorcraftApp, panel: &str) -> Option<String> {
|
||||
let (k, id) = parse(panel)?;
|
||||
(k.name)(app, id).map(|name| library_name(id, &name).to_string())
|
||||
}
|
||||
|
||||
/// Library panel `panel`'s contents (the dock or its floating group draws its tab and menu button).
|
||||
pub(crate) fn show(app: &mut VectorcraftApp, ui: &mut Ui, panel: &str) {
|
||||
if let Some((k, id)) = parse(panel) {
|
||||
(k.body)(app, ui, id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Library panel `panel`'s (≡) menu items.
|
||||
pub(crate) fn menu(app: &mut VectorcraftApp, ui: &mut Ui, panel: &str) {
|
||||
if let Some((k, id)) = parse(panel) {
|
||||
(k.menu)(app, ui, id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Per frame, before the panels are drawn: close the panels of libraries that are gone (a User
|
||||
/// Defined library whose file was deleted).
|
||||
pub fn tidy(app: &mut VectorcraftApp) {
|
||||
for panel in open_panels(&app.ui) {
|
||||
if parse(&panel).is_some_and(|(k, id)| (k.name)(app, id).is_none()) {
|
||||
close(app, &panel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn close(app: &mut VectorcraftApp, panel: &str) {
|
||||
if let Err(e) = app.run("window.panel.close", json!({ "panel": panel })) {
|
||||
app.status(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// At startup, after the preferences are restored: the persistent library panels reopen, where the
|
||||
/// saved layout has them or floating, their libraries read again (a loaded one from its file; one
|
||||
/// that can't be found is left out with a note in the status bar); the others closed with the app.
|
||||
pub fn restore(app: &mut VectorcraftApp) {
|
||||
let saved = std::mem::take(&mut app.ui.library_panels);
|
||||
let mut missing = vec![];
|
||||
for mut p in saved.into_iter().filter(|p| p.persistent).take(MAX_PANELS) {
|
||||
let Some(k) = kind(&p.kind) else { continue };
|
||||
if (k.name)(app, &p.id).is_none()
|
||||
&& let Some(path) = p.path.clone()
|
||||
// A file now in the user library folder loads as that User Defined library.
|
||||
&& let Ok(id) = app.run(k.load, json!({ "path": path })).map(|r| r["library"].as_str().unwrap_or_default().to_string())
|
||||
&& id != p.id
|
||||
&& rename(&mut app.ui, &p.panel(), &panel_id(k.kind, &id)).is_ok()
|
||||
{
|
||||
p.id = id;
|
||||
}
|
||||
match (k.name)(app, &p.id) {
|
||||
Some(_) => app.ui.library_panels.push(p),
|
||||
None => missing.push(p.path.unwrap_or(p.id)),
|
||||
}
|
||||
}
|
||||
let kept: Vec<String> = app.ui.library_panels.iter().map(LibraryPanel::panel).collect();
|
||||
for panel in open_panels(&app.ui).into_iter().filter(|p| !kept.contains(p)) {
|
||||
close(app, &panel);
|
||||
}
|
||||
app.ui.docking_hidden.retain(|p, _| !p.starts_with(PREFIX) || kept.contains(p));
|
||||
for panel in kept.into_iter().filter(|p| !is_open(&app.ui, p)).collect::<Vec<_>>() {
|
||||
if let Err(e) = float(app, &panel) {
|
||||
missing.push(format!("{panel} ({e})"));
|
||||
}
|
||||
}
|
||||
if !missing.is_empty() {
|
||||
let files = missing.join(", ");
|
||||
app.status(format!("Persistent libraries not found: {files}"));
|
||||
}
|
||||
}
|
||||
|
||||
/// Replace panel `from` with panel `to` (not open) where it is: in the dock or floating group it
|
||||
/// is in, and as the place it goes back to.
|
||||
fn rename(ui: &mut UiState, from: &str, to: &str) -> Result<(), String> {
|
||||
fn swap(v: &mut Value, from: &str, to: &str) {
|
||||
match v {
|
||||
Value::String(s) if s == from => *s = to.into(),
|
||||
Value::Array(a) => a.iter_mut().for_each(|v| swap(v, from, to)),
|
||||
Value::Object(o) => o.values_mut().for_each(|v| swap(v, from, to)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
// Panel ids are the only strings of a layout and a location.
|
||||
fn renamed<T: Serialize + serde::de::DeserializeOwned>(t: &T, from: &str, to: &str) -> Result<T, String> {
|
||||
let mut v = serde_json::to_value(t).map_err(|e| e.to_string())?;
|
||||
swap(&mut v, from, to);
|
||||
serde_json::from_value(v).map_err(|e| e.to_string())
|
||||
}
|
||||
registered(to)?;
|
||||
let docking = ui.docking.as_ref().map(|l| renamed(l, from, to)).transpose()?;
|
||||
let mut hidden = std::collections::BTreeMap::new();
|
||||
for (p, at) in &ui.docking_hidden {
|
||||
hidden.insert(if p == from { to.to_string() } else { p.clone() }, renamed(at, from, to)?);
|
||||
}
|
||||
ui.docking = docking;
|
||||
ui.docking_hidden = hidden;
|
||||
for p in ui.floating_panels.iter_mut().flat_map(|g| g.panels.iter_mut()).chain(ui.open_panel.as_mut()).filter(|p| p.as_str() == from) {
|
||||
*p = to.into();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The interaction id of the tile or row of `name` in panel `panel`.
|
||||
pub(crate) fn tile_id(panel: &str, name: &str) -> egui::Id {
|
||||
egui::Id::new(("library-tile", panel, name))
|
||||
}
|
||||
|
||||
/// The state key `key` of panel `panel`.
|
||||
fn key(panel: &str, key: &str) -> String {
|
||||
format!("{panel}-{key}")
|
||||
}
|
||||
|
||||
/// The submenu of the libraries menu that lists libraries of `category` (`None`: the top level).
|
||||
pub(crate) fn submenu(category: &str) -> Option<&'static str> {
|
||||
match category {
|
||||
@@ -125,22 +370,105 @@ pub(crate) fn library_name<'a>(id: &str, name: &'a str) -> &'a str {
|
||||
}
|
||||
|
||||
/// The opener of library `K`'s libraries (`window.swatchLibrary {library}`): open library `library`
|
||||
/// (an id or a name; `list`: the command listing them) in the panel, `null` closing it. `found`
|
||||
/// looks a library up: its id, name and item count. → {open, name, count}
|
||||
pub(crate) fn open_command<K: LibraryKind>(
|
||||
app: &mut VectorcraftApp,
|
||||
p: &Value,
|
||||
list: &str,
|
||||
found: impl FnOnce(&VectorcraftApp, &str) -> Option<(String, String, usize)>,
|
||||
) -> Result<Value, String> {
|
||||
/// (an id or a name) in its own panel, floating, or bring its panel forward when it is open;
|
||||
/// `null` closes every panel of `K`'s libraries. → {open, panel, name, count}
|
||||
pub(crate) fn open_command<K: LibraryKind>(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
|
||||
let Some(key) = p.get("library").filter(|v| !v.is_null()) else {
|
||||
app.ui.library_panel = None;
|
||||
for panel in open_panels(&app.ui).into_iter().filter(|p| parse(p).is_some_and(|(k, _)| k.kind == K::KIND)) {
|
||||
app.run("window.panel.close", json!({ "panel": panel }))?;
|
||||
}
|
||||
return Ok(json!({ "open": null }));
|
||||
};
|
||||
let list = K::LIST;
|
||||
let key = key.as_str().ok_or_else(|| format!("`library` is a library id or name (see {list})"))?;
|
||||
let (id, name, count) = found(app, key).ok_or_else(|| format!("no library `{key}` (see {list})"))?;
|
||||
app.ui.library_panel = Some(OpenLibrary { kind: K::KIND.into(), id: id.clone() });
|
||||
Ok(json!({"open": id, "name": name, "count": count}))
|
||||
let (id, name, count) = K::find(app, key).ok_or_else(|| format!("no library `{key}` (see {list})"))?;
|
||||
let panel = panel_id(K::KIND, &id);
|
||||
registered(&panel)?;
|
||||
// Brought forward, it is the one opened last.
|
||||
let at = app.ui.library_panels.iter().position(|q| q.kind == K::KIND && q.id == id);
|
||||
if let Some(s) = at.map(|i| app.ui.library_panels.remove(i)) {
|
||||
app.ui.library_panels.push(s);
|
||||
}
|
||||
settings_mut(app, kind_of::<K>(), &id);
|
||||
if is_open(&app.ui, &panel) {
|
||||
app.run("window.panel.activate", json!({ "panel": panel }))?;
|
||||
} else {
|
||||
float(app, &panel)?;
|
||||
}
|
||||
Ok(json!({"open": id, "panel": panel, "name": name, "count": count}))
|
||||
}
|
||||
|
||||
/// Float library panel `panel`, as tall as its body in the kind's view.
|
||||
pub(crate) fn float(app: &mut VectorcraftApp, panel: &str) -> Result<Value, String> {
|
||||
let large = parse(panel).is_some_and(|(k, _)| k.view == View::LargeThumb);
|
||||
app.run("window.panel.float", json!({ "panel": panel, "height": if large { 400 } else { 340 } }))
|
||||
}
|
||||
|
||||
/// `ui.libraryPanel {panel, library?, view?, showFind?, persistent?}`: show another library in an
|
||||
/// open library panel (Previous / Next Library; one open in a panel of its own is brought forward
|
||||
/// instead), and set a panel's view, Show Find Field and Persistent (also a closed persistent one's).
|
||||
/// → its settings
|
||||
pub(crate) fn command(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
|
||||
let given = p.get("panel").and_then(Value::as_str).unwrap_or_default();
|
||||
let (k, id) = parse(given).ok_or_else(|| format!("`{given}` isn't a library panel (see window.swatchLibrary)"))?;
|
||||
let mut id = id.to_string();
|
||||
let open = is_open(&app.ui, given);
|
||||
let closed = || format!("library panel `{given}` isn't open");
|
||||
if !open && !settings(&app.ui, k.kind, &id).is_some_and(|s| s.persistent) {
|
||||
return Err(closed());
|
||||
}
|
||||
let view = match p.get("view") {
|
||||
None | Some(Value::Null) => None,
|
||||
Some(v) => Some(
|
||||
serde_json::from_value::<View>(v.clone())
|
||||
.map_err(|_| "`view` is smallThumb, mediumThumb, largeThumb, smallList or largeList".to_string())?,
|
||||
),
|
||||
};
|
||||
let show_find = crate::menus::opt_bool(p, "showFind")?;
|
||||
let persistent = crate::menus::opt_bool(p, "persistent")?;
|
||||
if let Some(key) = p.get("library").filter(|v| !v.is_null()) {
|
||||
let list = k.list;
|
||||
let key = key.as_str().ok_or_else(|| format!("`library` is a library id or name (see {list})"))?;
|
||||
let (next, ..) = (k.find)(app, key).ok_or_else(|| format!("no library `{key}` (see {list})"))?;
|
||||
// Only an open panel shows another library.
|
||||
if !open {
|
||||
return Err(closed());
|
||||
}
|
||||
let to = panel_id(k.kind, &next);
|
||||
if is_open(&app.ui, &to) {
|
||||
app.run("window.panel.activate", json!({ "panel": to }))?;
|
||||
} else if next != id {
|
||||
rename(&mut app.ui, given, &to)?;
|
||||
app.ui.library_panels.retain(|q| !(q.kind == k.kind && q.id == next));
|
||||
let path = if next.starts_with("loaded/") { (k.path)(app, &next) } else { None };
|
||||
if let Some(s) = settings_mut(app, k, &id) {
|
||||
(s.id, s.path) = (next.clone(), path);
|
||||
}
|
||||
}
|
||||
id = next;
|
||||
}
|
||||
let s = settings_mut(app, k, &id).ok_or_else(closed)?;
|
||||
s.view = view.or(s.view);
|
||||
s.hide_find = show_find.map_or(s.hide_find, |show| !show);
|
||||
s.persistent = persistent.unwrap_or(s.persistent);
|
||||
let s = s.clone();
|
||||
Ok(json!({
|
||||
"panel": s.panel(),
|
||||
"kind": s.kind,
|
||||
"library": s.id,
|
||||
"view": s.view.unwrap_or(k.view),
|
||||
"showFind": !s.hide_find,
|
||||
"persistent": s.persistent,
|
||||
"open": is_open(&app.ui, &s.panel()),
|
||||
}))
|
||||
}
|
||||
|
||||
/// Run `ui.libraryPanel` on panel `panel` with `params`.
|
||||
fn set(app: &mut VectorcraftApp, panel: &str, mut params: Value) {
|
||||
params["panel"] = json!(panel);
|
||||
if let Err(e) = app.run("ui.libraryPanel", params) {
|
||||
app.status(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// Other Library…: the library file at `path`, else one picked in an open dialog; `None` when the
|
||||
@@ -167,73 +495,28 @@ pub(crate) fn user_slot(id: &str, prefix: &str, libraries: Vec<LibraryInfo>) ->
|
||||
libraries.into_iter().filter(|l| l.category == "user").nth(n.checked_sub(1)?)
|
||||
}
|
||||
|
||||
/// The panel's window: a tab strip with the library's name, the panel menu and a close button, then
|
||||
/// the body. A library that no longer exists closes it.
|
||||
fn window<K: LibraryKind>(app: &mut VectorcraftApp, ctx: &egui::Context, id: &str) {
|
||||
let Some((name, lib)) = K::get(app, id) else {
|
||||
app.ui.library_panel = None;
|
||||
return;
|
||||
};
|
||||
let t = Tokens::get(ctx);
|
||||
let mut open = true;
|
||||
// Left of the icon panels' pop-out, movable.
|
||||
let at = egui::pos2((ctx.content_rect().right() - 880.0).max(8.0), 140.0);
|
||||
let area =
|
||||
egui::Area::new(egui::Id::new(("library-panel", K::KIND))).order(egui::Order::Foreground).default_pos(at).movable(true).constrain(true);
|
||||
area.show(ctx, |ui| {
|
||||
egui::Frame::popup(ui.style()).fill(t.panel).corner_radius(CornerRadius::same(4)).inner_margin(egui::Margin::ZERO).show(ui, |ui| {
|
||||
ui.set_width(256.0);
|
||||
let (strip, _) = ui.allocate_exact_size(vec2(256.0, 26.0), Sense::hover());
|
||||
ui.painter().rect_filled(strip, CornerRadius { nw: 4, ne: 4, sw: 0, se: 0 }, t.panel_darker);
|
||||
let name = library_name(id, &name);
|
||||
let label = egui::RichText::new(name).font(theme::semibold(12.0));
|
||||
let galley = ui.painter().layout_no_wrap(name.to_string(), theme::semibold(12.0), t.text);
|
||||
let tab = Rect::from_min_size(strip.min, vec2((galley.size().x + 24.0).min(190.0), 26.0));
|
||||
ui.painter().rect_filled(tab, CornerRadius { nw: 4, ne: 0, sw: 0, se: 0 }, t.panel);
|
||||
ui.put(tab.shrink2(vec2(12.0, 0.0)), egui::Label::new(label.color(t.text)).truncate().selectable(false));
|
||||
let close = Rect::from_center_size(strip.right_center() - vec2(13.0, 0.0), vec2(14.0, 14.0));
|
||||
let cr = ui.interact(close, ui.id().with("close-library"), Sense::click()).on_hover_text(tl!("Close"));
|
||||
icons::paint(ui, "x", close, if cr.hovered() { t.text } else { t.text_dim });
|
||||
open = !cr.clicked();
|
||||
let menu = Rect::from_center_size(strip.right_center() - vec2(34.0, 0.0), vec2(16.0, 16.0));
|
||||
let mr = ui.interact(menu, ui.id().with("library-menu"), Sense::click()).on_hover_text(tl!("Panel menu"));
|
||||
icons::paint(ui, "menu", menu.shrink(1.0), if mr.hovered() { t.text_strong } else { t.text_dim });
|
||||
egui::Popup::menu(&mr).show(|ui| {
|
||||
ui.set_min_width(200.0);
|
||||
panel_menu::<K>(app, ui, id);
|
||||
});
|
||||
egui::Frame::NONE.inner_margin(egui::Margin::same(10)).show(ui, |ui| {
|
||||
ui.set_width(236.0);
|
||||
body::<K>(app, ui, id, &lib);
|
||||
});
|
||||
});
|
||||
});
|
||||
if !open {
|
||||
app.ui.library_panel = None;
|
||||
}
|
||||
/// Test frames: the panel opened last, as the dock or a floating group draws its body.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn show_window(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
let Some(panel) = last_open(&app.ui).map(LibraryPanel::panel) else { return };
|
||||
let area = egui::Area::new(egui::Id::new("test-library-panel")).fixed_pos(egui::pos2(20.0, 20.0));
|
||||
area.show(ctx, |ui| crate::dock::panel_body(app, ui, &panel, 256.0, 400.0));
|
||||
}
|
||||
|
||||
/// The selected items and folders of library `id` (a selection in another library is dropped).
|
||||
fn selection<K: LibraryKind>(ctx: &egui::Context, id: &str) -> Vec<String> {
|
||||
let (lib, sel): (String, Vec<String>) = pstate(ctx, &key::<K>("selected"));
|
||||
if lib == id { sel } else { vec![] }
|
||||
/// The selected items and folders of panel `panel`.
|
||||
fn selection(ctx: &egui::Context, panel: &str) -> Vec<String> {
|
||||
pstate(ctx, &key(panel, "selected"))
|
||||
}
|
||||
|
||||
fn set_selection<K: LibraryKind>(ctx: &egui::Context, id: &str, sel: Vec<String>) {
|
||||
set_pstate(ctx, &key::<K>("selected"), (id.to_string(), sel));
|
||||
}
|
||||
|
||||
/// Find field, thumbnails or list rows, and the bottom bar.
|
||||
fn body<K: LibraryKind>(app: &mut VectorcraftApp, ui: &mut Ui, id: &str, lib: &K::Lib) {
|
||||
let view = view::<K>(ui.ctx());
|
||||
let query = if pstate::<bool>(ui.ctx(), &key::<K>("hide-find")) {
|
||||
String::new()
|
||||
} else {
|
||||
widgets::search_field(ui, egui::Id::new(key::<K>("find")), tl!("Find"))
|
||||
};
|
||||
/// Find field, thumbnails or list rows, and the bottom bar of library `id`'s panel.
|
||||
fn body<K: LibraryKind>(app: &mut VectorcraftApp, ui: &mut Ui, id: &str) {
|
||||
let Some((_, lib)) = K::get(app, id) else { return };
|
||||
let panel = panel_id(K::KIND, id);
|
||||
let (view, hide_find) = settings(&app.ui, K::KIND, id).map_or((K::VIEW, false), |s| (s.view.unwrap_or(K::VIEW), s.hide_find));
|
||||
let query = if hide_find { String::new() } else { widgets::search_field(ui, egui::Id::new(key(&panel, "find")), tl!("Find")) };
|
||||
ui.add_space(4.0);
|
||||
let rows = K::rows(lib, &query.trim().to_lowercase());
|
||||
let selected = selection::<K>(ui.ctx(), id);
|
||||
let rows = K::rows(&lib, &query.trim().to_lowercase());
|
||||
let selected = selection(ui.ctx(), &panel);
|
||||
let is_sel = |n: &str| selected.iter().any(|s| s == n);
|
||||
let mut clicked: Option<(String, bool, egui::Modifiers)> = None;
|
||||
let mut click = |ui: &Ui, resp: egui::Response, row: &Row<K::Item>, tip: &dyn Fn() -> String| {
|
||||
@@ -243,13 +526,13 @@ fn body<K: LibraryKind>(app: &mut VectorcraftApp, ui: &mut Ui, id: &str, lib: &K
|
||||
};
|
||||
widgets::list_box(ui, |ui| {
|
||||
let max_height = if view == View::LargeThumb { 280.0 } else { 220.0 };
|
||||
egui::ScrollArea::vertical().id_salt(key::<K>("scroll")).max_height(max_height).show(ui, |ui| {
|
||||
egui::ScrollArea::vertical().id_salt(key(&panel, "scroll")).max_height(max_height).show(ui, |ui| {
|
||||
ui.set_width(ui.available_width());
|
||||
if rows.is_empty() {
|
||||
widgets::dim_label(ui, if query.trim().is_empty() { tl!("This library is empty.") } else { tl!("No matches.") });
|
||||
} else if view.is_list() {
|
||||
for row in &rows {
|
||||
let (r, resp, chip) = list_row(ui, tile_id::<K>(row.name), view, is_sel(row.name));
|
||||
let (r, resp, chip) = list_row(ui, tile_id(&panel, row.name), view, is_sel(row.name));
|
||||
match row.item {
|
||||
Some(item) => {
|
||||
K::draw(app, ui, chip, item, false, false);
|
||||
@@ -263,7 +546,7 @@ fn body<K: LibraryKind>(app: &mut VectorcraftApp, ui: &mut Ui, id: &str, lib: &K
|
||||
}
|
||||
} else {
|
||||
for (row, cell) in tile_grid(ui, &rows, view, |r| r.item.is_none()) {
|
||||
let resp = ui.interact(cell, tile_id::<K>(row.name), Sense::click());
|
||||
let resp = ui.interact(cell, tile_id(&panel, row.name), Sense::click());
|
||||
match row.item {
|
||||
Some(item) => K::draw(app, ui, cell, item, is_sel(row.name), resp.hovered()),
|
||||
None => folder_tile(ui, cell, is_sel(row.name)),
|
||||
@@ -277,8 +560,8 @@ fn body<K: LibraryKind>(app: &mut VectorcraftApp, ui: &mut Ui, id: &str, lib: &K
|
||||
// A plain click on an item also adds and applies it; folders and modifier clicks only select.
|
||||
let only_select = folder || m.shift || m.command;
|
||||
let order: Vec<&str> = rows.iter().map(|r| r.name).collect();
|
||||
let sel = click_selection(ui, &key::<K>("anchor"), selected, &order, &name, m);
|
||||
set_selection::<K>(ui.ctx(), id, sel);
|
||||
let sel = click_selection(ui, &key(&panel, "anchor"), selected, &order, &name, m);
|
||||
set_pstate(ui.ctx(), &key(&panel, "selected"), sel);
|
||||
if !only_select {
|
||||
K::click(app, ui, id, &name);
|
||||
}
|
||||
@@ -294,7 +577,14 @@ fn neighbour<K: LibraryKind>(app: &VectorcraftApp, id: &str, step: isize) -> Opt
|
||||
all.into_iter().nth((i + step).rem_euclid(n) as usize).map(|l| l.id)
|
||||
}
|
||||
|
||||
/// Open library `id` in the panel.
|
||||
/// Show the library `step` places after (or before) `id` in its panel.
|
||||
fn step<K: LibraryKind>(app: &mut VectorcraftApp, id: &str, step: isize) {
|
||||
if let Some(next) = neighbour::<K>(app, id, step) {
|
||||
set(app, &panel_id(K::KIND, id), json!({ "library": next }));
|
||||
}
|
||||
}
|
||||
|
||||
/// Open library `id` in its panel.
|
||||
fn open<K: LibraryKind>(app: &mut VectorcraftApp, id: &str) {
|
||||
if let Err(e) = app.run(K::OPEN, json!({ "library": id })) {
|
||||
app.status(e);
|
||||
@@ -303,18 +593,16 @@ fn open<K: LibraryKind>(app: &mut VectorcraftApp, id: &str) {
|
||||
|
||||
/// Libraries menu, previous / next library, and Add to … for the selection.
|
||||
fn bottom<K: LibraryKind>(app: &mut VectorcraftApp, ui: &mut Ui, id: &str) {
|
||||
let selected = selection::<K>(ui.ctx(), id);
|
||||
let selected = selection(ui.ctx(), &panel_id(K::KIND, id));
|
||||
widgets::bottom_bar(ui, |ui| {
|
||||
let r = widgets::icon_button(ui, "library", K::MENU, false, 24.0);
|
||||
egui::Popup::menu(&r).show(|ui| {
|
||||
ui.set_min_width(200.0);
|
||||
library_menu::<K>(app, ui);
|
||||
});
|
||||
for (icon, tip, step) in [("chevron-left", tl!("Previous Library"), -1), ("chevron-right", tl!("Next Library"), 1)] {
|
||||
if widgets::icon_button(ui, icon, tip, false, 24.0).clicked()
|
||||
&& let Some(next) = neighbour::<K>(app, id, step)
|
||||
{
|
||||
open::<K>(app, &next);
|
||||
for (icon, tip, s) in [("chevron-left", tl!("Previous Library"), -1), ("chevron-right", tl!("Next Library"), 1)] {
|
||||
if widgets::icon_button(ui, icon, tip, false, 24.0).clicked() {
|
||||
step::<K>(app, id, s);
|
||||
}
|
||||
}
|
||||
ui.add_space((ui.available_width() - 28.0).max(0.0));
|
||||
@@ -324,54 +612,56 @@ fn bottom<K: LibraryKind>(app: &mut VectorcraftApp, ui: &mut Ui, id: &str) {
|
||||
});
|
||||
}
|
||||
|
||||
/// The library panel's (≡) menu.
|
||||
/// A library panel's (≡) menu.
|
||||
fn panel_menu<K: LibraryKind>(app: &mut VectorcraftApp, ui: &mut Ui, id: &str) {
|
||||
let selected = selection::<K>(ui.ctx(), id);
|
||||
let panel = panel_id(K::KIND, id);
|
||||
let selected = selection(ui.ctx(), &panel);
|
||||
if menu_item(ui, K::ADD, !selected.is_empty(), false) {
|
||||
K::add(app, id, selected);
|
||||
}
|
||||
ui.separator();
|
||||
let view = view::<K>(ui.ctx());
|
||||
let s = settings(&app.ui, K::KIND, id);
|
||||
let (view, hide_find, persistent) = s.map_or((K::VIEW, false, false), |s| (s.view.unwrap_or(K::VIEW), s.hide_find, s.persistent));
|
||||
for (v, label) in View::ALL {
|
||||
if menu_item(ui, label, true, v == view) {
|
||||
set_pstate(ui.ctx(), &key::<K>("view"), Some(v));
|
||||
set(app, &panel, json!({ "view": v }));
|
||||
}
|
||||
}
|
||||
ui.separator();
|
||||
let hidden: bool = pstate(ui.ctx(), &key::<K>("hide-find"));
|
||||
if menu_item(ui, tl!("Show Find Field"), true, !hidden) {
|
||||
set_pstate(ui.ctx(), &key::<K>("hide-find"), !hidden);
|
||||
if menu_item(ui, tl!("Show Find Field"), true, !hide_find) {
|
||||
set(app, &panel, json!({ "showFind": hide_find }));
|
||||
}
|
||||
ui.separator();
|
||||
for (label, step) in [(tl!("Previous Library"), -1), (tl!("Next Library"), 1)] {
|
||||
if menu_item(ui, label, true, false)
|
||||
&& let Some(next) = neighbour::<K>(app, id, step)
|
||||
{
|
||||
open::<K>(app, &next);
|
||||
for (label, s) in [(tl!("Previous Library"), -1), (tl!("Next Library"), 1)] {
|
||||
if menu_item(ui, label, true, false) {
|
||||
step::<K>(app, id, s);
|
||||
}
|
||||
}
|
||||
if menu_item(ui, tl!("Persistent"), true, persistent) {
|
||||
set(app, &panel, json!({ "persistent": !persistent }));
|
||||
}
|
||||
if menu_item(ui, tl!("Close Library"), true, false) {
|
||||
app.ui.library_panel = None;
|
||||
close(app, &panel);
|
||||
}
|
||||
}
|
||||
|
||||
/// The libraries menu (the panel's library button, the Swatches panel's): every library by
|
||||
/// submenu, the open one checked, between `K`'s head and tail items.
|
||||
/// The libraries menu (a library panel's library button, the Swatches panel's): every library by
|
||||
/// submenu, the open ones checked, between `K`'s head and tail items.
|
||||
pub(crate) fn library_menu<K: LibraryKind>(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
K::menu_head(app, ui);
|
||||
let current = app.ui.library_panel.as_ref().filter(|o| o.kind == K::KIND).map(|o| o.id.clone());
|
||||
if let Some(id) = library_items(ui, &K::list(app), current.as_deref()) {
|
||||
let shown = open_panels(&app.ui);
|
||||
if let Some(id) = library_items(ui, &K::list(app), &|id| shown.contains(&panel_id(K::KIND, id))) {
|
||||
open::<K>(app, &id);
|
||||
}
|
||||
K::menu_tail(app, ui);
|
||||
}
|
||||
|
||||
/// Menu rows for `libs`: the top-level ones, then a submenu per [`LibraryRef::submenu`], with
|
||||
/// `current` checked. Returns the id of the one clicked.
|
||||
pub(crate) fn library_items(ui: &mut Ui, libs: &[LibraryRef], current: Option<&str>) -> Option<String> {
|
||||
/// Menu rows for `libs`: the top-level ones, then a submenu per [`LibraryRef::submenu`], those
|
||||
/// `checked` says checked. Returns the id of the one clicked.
|
||||
pub(crate) fn library_items(ui: &mut Ui, libs: &[LibraryRef], checked: &dyn Fn(&str) -> bool) -> Option<String> {
|
||||
let mut chosen = None;
|
||||
let mut item = |ui: &mut Ui, l: &LibraryRef| {
|
||||
if widgets::menu_item_name(ui, library_name(&l.id, &l.name), true, current == Some(l.id.as_str())) {
|
||||
if widgets::menu_item_name(ui, library_name(&l.id, &l.name), true, checked(&l.id)) {
|
||||
chosen = Some(l.id.clone());
|
||||
}
|
||||
};
|
||||
@@ -398,7 +688,7 @@ pub(crate) fn library_items(ui: &mut Ui, libs: &[LibraryRef], current: Option<&s
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use egui::{Event, Modifiers, PointerButton, Pos2};
|
||||
use egui::{Event, Modifiers, PointerButton, Pos2, vec2};
|
||||
use serde_json::Value;
|
||||
use vectorcraft_color::Paint;
|
||||
use vectorcraft_engine::Session;
|
||||
@@ -427,12 +717,17 @@ mod tests {
|
||||
out.textures_delta.clear();
|
||||
}
|
||||
|
||||
/// Click the tile or row of `name` with `modifiers` (frames from `time` to `time + 0.5`; a new
|
||||
/// window takes a sizing frame first).
|
||||
/// The panel opened last.
|
||||
fn shown(app: &VectorcraftApp) -> String {
|
||||
last_open(&app.ui).expect("a library panel is open").panel()
|
||||
}
|
||||
|
||||
/// Click the tile or row of `name` in the panel opened last with `modifiers` (frames from
|
||||
/// `time` to `time + 0.5`; a new window takes a sizing frame first).
|
||||
fn click(app: &mut VectorcraftApp, ctx: &egui::Context, name: &str, modifiers: Modifiers, time: f64) {
|
||||
frame(app, ctx, vec![], Modifiers::NONE, time);
|
||||
frame(app, ctx, vec![], Modifiers::NONE, time + 0.25);
|
||||
let at = ctx.read_response(tile_id::<SwatchLibraries>(name)).unwrap_or_else(|| panic!("no tile for {name}")).rect.center();
|
||||
let at = ctx.read_response(tile_id(&shown(app), name)).unwrap_or_else(|| panic!("no tile for {name}")).rect.center();
|
||||
let button = |pressed| Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers };
|
||||
frame(app, ctx, vec![Event::PointerMoved(at), button(true), button(false)], modifiers, time + 0.5);
|
||||
}
|
||||
@@ -446,8 +741,8 @@ mod tests {
|
||||
let mut app = app();
|
||||
let ctx = context();
|
||||
let r = app.run("window.swatchLibrary", json!({"library": "Pastels"})).unwrap();
|
||||
assert_eq!((r["open"].as_str(), r["count"].as_u64()), (Some("pastels"), Some(24)));
|
||||
assert_eq!(app.ui.library_panel, Some(OpenLibrary { kind: "swatches".into(), id: "pastels".into() }));
|
||||
assert_eq!((r["open"].as_str(), r["count"].as_u64(), r["panel"].as_str()), (Some("pastels"), Some(24), Some("library:swatches:pastels")));
|
||||
assert_eq!(last_open(&app.ui).map(|p| (p.kind.as_str(), p.id.as_str())), Some(("swatches", "pastels")));
|
||||
let undo = undo_len(&app);
|
||||
click(&mut app, &ctx, "Pastel Teal", Modifiers::NONE, 0.0);
|
||||
let d = &app.session.doc().unwrap().doc;
|
||||
@@ -459,10 +754,10 @@ mod tests {
|
||||
let blue = app.session.doc().unwrap().doc.swatch("Soft Blue").and_then(|w| w.paint.color()).unwrap();
|
||||
assert_eq!(app.session.paint.stroke.color(), Some(blue));
|
||||
assert!(app.session.fill_active, "Alt keeps the active proxy");
|
||||
// A library that doesn't exist closes the panel; null closes it.
|
||||
// A library that doesn't exist is an error; null closes the panel.
|
||||
assert!(app.run("window.swatchLibrary", json!({"library": "nope"})).is_err());
|
||||
app.run("window.swatchLibrary", json!({"library": null})).unwrap();
|
||||
assert!(app.ui.library_panel.is_none());
|
||||
assert!(last_open(&app.ui).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -472,17 +767,17 @@ mod tests {
|
||||
app.run("window.swatchLibrary", json!({"library": "earth-tones"})).unwrap();
|
||||
click(&mut app, &ctx, "Clay 1", Modifiers::COMMAND, 0.0);
|
||||
click(&mut app, &ctx, "Ochre", Modifiers::NONE, 2.0);
|
||||
assert_eq!(selection::<SwatchLibraries>(&ctx, "earth-tones"), ["Ochre"], "a folder click selects its group");
|
||||
assert_eq!(selection(&ctx, "library:swatches:earth-tones"), ["Ochre"], "a folder click selects its group");
|
||||
click(&mut app, &ctx, "Clay 3", Modifiers::COMMAND, 4.0);
|
||||
assert!(app.session.doc().unwrap().doc.swatch("Clay 3").is_none(), "modifier clicks only select");
|
||||
let sel = selection::<SwatchLibraries>(&ctx, "earth-tones");
|
||||
let sel = selection(&ctx, "library:swatches:earth-tones");
|
||||
SwatchLibraries::add(&mut app, "earth-tones", sel);
|
||||
let d = &app.session.doc().unwrap().doc;
|
||||
assert_eq!(d.swatch_groups.iter().find(|g| g.name == "Ochre").map(|g| g.swatches.len()), Some(6));
|
||||
assert!(d.swatches.iter().any(|w| w.name == "Clay 3"));
|
||||
// The selection belongs to its library.
|
||||
// The selection belongs to its library's panel.
|
||||
app.run("window.swatchLibrary", json!({"library": "brights"})).unwrap();
|
||||
assert!(selection::<SwatchLibraries>(&ctx, "brights").is_empty());
|
||||
assert!(selection(&ctx, "library:swatches:brights").is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -494,12 +789,12 @@ mod tests {
|
||||
let order: Vec<String> = SwatchLibraries::rows(&lib, "").iter().map(|r| r.name.to_string()).collect();
|
||||
click(&mut app, &ctx, &order[1], Modifiers::NONE, 0.0);
|
||||
click(&mut app, &ctx, &order[4], Modifiers::SHIFT, 2.0);
|
||||
assert_eq!(selection::<SwatchLibraries>(&ctx, "pastels"), order[1..=4], "the range starts at the plain click");
|
||||
assert_eq!(selection(&ctx, "library:swatches:pastels"), order[1..=4], "the range starts at the plain click");
|
||||
// A plain click after a Cmd/Ctrl-click moves the anchor from the Cmd/Ctrl-clicked item.
|
||||
click(&mut app, &ctx, &order[8], Modifiers::COMMAND, 4.0);
|
||||
click(&mut app, &ctx, &order[2], Modifiers::NONE, 6.0);
|
||||
click(&mut app, &ctx, &order[3], Modifiers::SHIFT, 8.0);
|
||||
assert_eq!(selection::<SwatchLibraries>(&ctx, "pastels"), order[2..=3]);
|
||||
assert_eq!(selection(&ctx, "library:swatches:pastels"), order[2..=3]);
|
||||
let d = &app.session.doc().unwrap().doc;
|
||||
assert!(d.swatch(&order[2]).is_some(), "a plain click still adds the swatch");
|
||||
assert!(d.swatch(&order[3]).is_none(), "a Shift-click only selects");
|
||||
@@ -510,12 +805,13 @@ mod tests {
|
||||
let mut app = app();
|
||||
let ctx = context();
|
||||
app.run("window.swatchLibrary", json!({"library": "web-safe-216"})).unwrap();
|
||||
set_pstate(&ctx, &key::<SwatchLibraries>("view"), Some(View::SmallList));
|
||||
ctx.data_mut(|d| d.insert_temp(egui::Id::new(key::<SwatchLibraries>("find")), "#FF00".to_string()));
|
||||
let panel = "library:swatches:web-safe-216";
|
||||
app.run("ui.libraryPanel", json!({"panel": panel, "view": "smallList"})).unwrap();
|
||||
ctx.data_mut(|d| d.insert_temp(egui::Id::new(key(panel, "find")), "#FF00".to_string()));
|
||||
frame(&mut app, &ctx, vec![], Modifiers::NONE, 0.0);
|
||||
frame(&mut app, &ctx, vec![], Modifiers::NONE, 0.5);
|
||||
assert!(ctx.read_response(tile_id::<SwatchLibraries>("#FF0033")).is_some());
|
||||
assert!(ctx.read_response(tile_id::<SwatchLibraries>("#00FF00")).is_none());
|
||||
assert!(ctx.read_response(tile_id(panel, "#FF0033")).is_some());
|
||||
assert!(ctx.read_response(tile_id(panel, "#00FF00")).is_none());
|
||||
let first = SWATCH_IDS[0];
|
||||
let last = SwatchLibraries::list(&app).pop().unwrap().id;
|
||||
assert_eq!(last, "radial-glows", "gradient libraries come after the swatch libraries");
|
||||
@@ -585,7 +881,7 @@ mod tests {
|
||||
assert!(app.ui.dialog.is_none());
|
||||
assert_eq!(user_items(&app), [("Loud".to_string(), "window.userSwatchLibrary1".to_string())]);
|
||||
app.run("window.userSwatchLibrary1", json!({})).unwrap();
|
||||
assert_eq!(app.ui.library_panel.as_ref().map(|o| o.id.as_str()), Some("user/Loud.vcswatches"));
|
||||
assert_eq!(last_open(&app.ui).map(|p| p.id.as_str()), Some("user/Loud.vcswatches"));
|
||||
let (_, lib) = SwatchLibraries::get(&app, "user/Loud.vcswatches").unwrap();
|
||||
assert_eq!((lib.groups.len(), lib.len()), (1, 5), "only the selected group");
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
@@ -625,7 +921,7 @@ mod tests {
|
||||
);
|
||||
// Opening a palette file opens it in the library panel.
|
||||
crate::io::open_bytes(&mut app, "Saved.gpl", text.as_bytes(), None).unwrap();
|
||||
let open = app.ui.library_panel.clone().unwrap();
|
||||
let open = last_open(&app.ui).cloned().unwrap();
|
||||
assert!(open.id.starts_with("loaded/"), "{open:?}");
|
||||
assert_eq!(app.session.documents().len(), 1, "not opened as a document");
|
||||
let other = SwatchLibraries::list(&app).into_iter().find(|l| l.id == open.id).unwrap();
|
||||
@@ -688,10 +984,160 @@ mod tests {
|
||||
// A file opened on the web has no path; open_bytes passes its bytes as dataBase64.
|
||||
let mut app = app();
|
||||
crate::io::open_bytes(&mut app, "Brand.ase", &vectorcraft_testkit::ase::sample(), None).unwrap();
|
||||
let open = app.ui.library_panel.clone().unwrap();
|
||||
let open = last_open(&app.ui).cloned().unwrap();
|
||||
assert_eq!(open.id, "loaded/Brand.ase");
|
||||
let (_, lib) = SwatchLibraries::get(&app, &open.id).unwrap();
|
||||
assert_eq!((lib.len(), lib.groups.len()), (4, 1));
|
||||
assert_eq!(app.session.documents().len(), 1, "not opened as a document");
|
||||
}
|
||||
|
||||
fn docking(app: &VectorcraftApp) -> &craft_ui::docking::Layout<String> {
|
||||
app.ui.docking.as_ref().expect("library panels live in the shared layout")
|
||||
}
|
||||
|
||||
fn panel_of(reply: Value) -> String {
|
||||
reply["panel"].as_str().expect("the reply names the panel").to_string()
|
||||
}
|
||||
|
||||
fn groups(app: &VectorcraftApp) -> Vec<Vec<String>> {
|
||||
docking(app).floating.iter().map(|g| g.panels.clone()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_library_opens_in_a_panel_of_its_own() {
|
||||
let mut app = app();
|
||||
// Without the shared layout yet, `window.panel` floats a library panel too.
|
||||
app.run("window.panel", json!({"panel": "library:swatches:earth-tones"})).unwrap();
|
||||
assert!(is_open(&app.ui, "library:swatches:earth-tones"));
|
||||
app.run("window.panel.close", json!({"panel": "library:swatches:earth-tones"})).unwrap();
|
||||
let pastels = panel_of(app.run("window.swatchLibrary", json!({"library": "pastels"})).unwrap());
|
||||
let brights = panel_of(app.run("window.swatchLibrary", json!({"library": "Brights"})).unwrap());
|
||||
let styles = panel_of(app.run("window.graphicStyleLibrary", json!({"library": "hand-drawn"})).unwrap());
|
||||
assert_eq!([&pastels, &brights, &styles], ["library:swatches:pastels", "library:swatches:brights", "library:graphicStyles:hand-drawn"]);
|
||||
assert_eq!(groups(&app), [vec![pastels.clone()], vec![brights.clone()], vec![styles.clone()]], "each floats on its own");
|
||||
assert_ne!(docking(&app).floating[0].rect, docking(&app).floating[1].rect, "one beside another");
|
||||
assert_eq!(tab_label(&app, &brights).as_deref(), Some("Brights"));
|
||||
// Opening an open library brings its panel forward.
|
||||
app.run("window.swatchLibrary", json!({"library": "pastels"})).unwrap();
|
||||
assert_eq!(groups(&app).len(), 3);
|
||||
assert_eq!(last_open(&app.ui).map(LibraryPanel::panel), Some(pastels.clone()));
|
||||
// They group, dock and close as any panel, and `window.panel` shows one again.
|
||||
app.run("window.panel.float", json!({"panel": brights, "onto": pastels})).unwrap();
|
||||
assert!(groups(&app).contains(&vec![pastels.clone(), brights.clone()]), "{:?}", groups(&app));
|
||||
app.run("window.panel.dock", json!({"panel": styles})).unwrap();
|
||||
assert!(craft_ui::docking::Layout { root: docking(&app).root.clone(), floating: vec![] }.contains(&styles));
|
||||
app.run("window.panel.close", json!({"panel": brights})).unwrap();
|
||||
assert!(!is_open(&app.ui, &brights) && is_open(&app.ui, &pastels));
|
||||
app.run("window.panel", json!({"panel": brights})).unwrap();
|
||||
assert!(is_open(&app.ui, &brights));
|
||||
assert!(crate::panel_docking::valid(docking(&app)));
|
||||
// `library: null` closes the panels of its kind.
|
||||
app.run("window.swatchLibrary", json!({"library": null})).unwrap();
|
||||
assert!(!any_open::<SwatchLibraries>(&app.ui) && is_open(&app.ui, &styles));
|
||||
// What isn't a library panel's id.
|
||||
for bad in ["library:nope:x", "library:swatches:", "library:swatches", "Library:swatches:pastels"] {
|
||||
assert!(crate::menus::normalize_panel(bad).is_none(), "{bad}");
|
||||
}
|
||||
assert!(app.run("ui.libraryPanel", json!({"panel": "layers"})).is_err());
|
||||
assert!(app.run("ui.libraryPanel", json!({"panel": pastels, "persistent": true})).is_err(), "closed and not persistent");
|
||||
assert!(app.run("ui.libraryPanel", json!({"panel": styles, "view": "huge"})).is_err());
|
||||
}
|
||||
|
||||
/// The app restarted with the preferences `app` saves.
|
||||
fn restarted(app: &VectorcraftApp) -> VectorcraftApp {
|
||||
let saved: crate::UiState = serde_json::from_slice(&serde_json::to_vec(&app.ui).unwrap()).unwrap();
|
||||
let mut next = VectorcraftApp::new(Session::new(), Default::default());
|
||||
next.ui = saved.sanitized();
|
||||
restore(&mut next);
|
||||
next
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn persistent_panels_reopen_at_startup_with_their_settings() {
|
||||
let dir = std::env::temp_dir().join(format!("vc-persistent-libraries-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let gpl = dir.join("Brand.gpl");
|
||||
std::fs::write(&gpl, "GIMP Palette\nName: Brand\n#\n255 0 0\tRed\n").unwrap();
|
||||
let mut app = app();
|
||||
let pastels = panel_of(app.run("window.swatchLibrary", json!({"library": "pastels"})).unwrap());
|
||||
let brights = panel_of(app.run("window.swatchLibrary", json!({"library": "brights"})).unwrap());
|
||||
let brand = panel_of(app.run("window.swatchLibrary.other", json!({"path": gpl.to_string_lossy()})).unwrap());
|
||||
let r = app.run("ui.libraryPanel", json!({"panel": pastels, "persistent": true, "view": "largeList", "showFind": false})).unwrap();
|
||||
assert_eq!((r["persistent"].as_bool(), r["view"].as_str(), r["showFind"].as_bool()), (Some(true), Some("largeList"), Some(false)));
|
||||
app.run("ui.libraryPanel", json!({"panel": brand, "persistent": true})).unwrap();
|
||||
app.run("window.panel.float", json!({"panel": pastels, "x": 40, "y": 90})).unwrap();
|
||||
// A persistent panel reopens even when it was closed.
|
||||
app.run("window.panel.close", json!({"panel": brand})).unwrap();
|
||||
let next = restarted(&app);
|
||||
assert!(is_open(&next.ui, &pastels) && is_open(&next.ui, &brand), "{}", next.ui.status);
|
||||
assert!(!is_open(&next.ui, &brights), "a panel that isn't persistent closed with the app");
|
||||
let group = docking(&next).floating.iter().find(|g| g.panels.contains(&pastels)).unwrap();
|
||||
assert_eq!(group.rect[..2], [40.0, 90.0], "where it was");
|
||||
let s = settings(&next.ui, "swatches", "pastels").unwrap();
|
||||
assert_eq!((s.view, s.hide_find, s.persistent), (Some(View::LargeList), true, true));
|
||||
let (_, id) = parse(&brand).unwrap();
|
||||
assert!(SwatchLibraries::get(&next, id).is_some_and(|(_, lib)| lib.swatch("Red").is_some()), "read again from its file");
|
||||
assert!(next.ui.status.is_empty(), "{}", next.ui.status);
|
||||
assert!(next.ui.docking_hidden.keys().all(|p| p != &brights));
|
||||
// Unchecked, a panel no longer reopens; a file that is gone is left out with a note.
|
||||
let mut next = next;
|
||||
next.run("ui.libraryPanel", json!({"panel": pastels, "persistent": false})).unwrap();
|
||||
std::fs::remove_file(&gpl).unwrap();
|
||||
let last = restarted(&next);
|
||||
assert!(!is_open(&last.ui, &pastels) && !is_open(&last.ui, &brand));
|
||||
assert!(last.ui.status.contains("Brand.gpl"), "{}", last.ui.status);
|
||||
assert!(last.ui.library_panels.is_empty());
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn previous_and_next_show_the_neighbouring_library_in_the_same_panel() {
|
||||
let mut app = app();
|
||||
let first = panel_of(app.run("window.swatchLibrary", json!({"library": "pastels"})).unwrap());
|
||||
app.run("window.panel.float", json!({"panel": first, "x": 50, "y": 60})).unwrap();
|
||||
app.run("ui.libraryPanel", json!({"panel": first, "persistent": true, "view": "smallList"})).unwrap();
|
||||
step::<SwatchLibraries>(&mut app, "pastels", 1);
|
||||
let next = panel_id("swatches", SWATCH_IDS[5]);
|
||||
assert!(!is_open(&app.ui, &first) && is_open(&app.ui, &next), "{}", app.ui.status);
|
||||
let group = docking(&app).floating.iter().find(|g| g.panels.contains(&next)).unwrap();
|
||||
assert_eq!((group.panels.len(), group.rect[0], group.rect[1]), (1, 50.0, 60.0), "in its place");
|
||||
let s = settings(&app.ui, "swatches", SWATCH_IDS[5]).unwrap();
|
||||
assert_eq!((s.view, s.persistent), (Some(View::SmallList), true), "the panel keeps its settings");
|
||||
assert!(settings(&app.ui, "swatches", "pastels").is_none());
|
||||
assert!(crate::panel_docking::valid(docking(&app)));
|
||||
// A library open in a panel of its own is brought forward instead.
|
||||
app.run("window.swatchLibrary", json!({"library": "pastels"})).unwrap();
|
||||
let r = app.run("ui.libraryPanel", json!({"panel": next, "library": "Pastels"})).unwrap();
|
||||
assert_eq!(r["panel"], json!(first));
|
||||
assert!(is_open(&app.ui, &next) && is_open(&app.ui, &first));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_panel_closes_when_its_library_is_gone() {
|
||||
let dir = std::env::temp_dir().join(format!("vc-gone-library-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
std::fs::write(dir.join("Gone.gpl"), "GIMP Palette\nName: Gone\n#\n0 0 0\tInk\n").unwrap();
|
||||
let mut app = app();
|
||||
app.session.swatch_libraries.set_user_dir(Some(dir.to_string_lossy().to_string()));
|
||||
let panel = panel_of(app.run("window.swatchLibrary", json!({"library": "user/Gone.gpl"})).unwrap());
|
||||
tidy(&mut app);
|
||||
assert!(is_open(&app.ui, &panel));
|
||||
std::fs::remove_file(dir.join("Gone.gpl")).unwrap();
|
||||
app.session.swatch_libraries.rescan();
|
||||
tidy(&mut app);
|
||||
assert!(!is_open(&app.ui, &panel));
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_panel_menu_has_persistent() {
|
||||
let mut app = app();
|
||||
let panel = panel_of(app.run("window.swatchLibrary", json!({"library": "pastels"})).unwrap());
|
||||
let text = crate::tests_labels::painted_text(&mut app, |app, ui| menu(app, ui, &panel));
|
||||
for item in ["Add to Swatches", "Small List View", "Show Find Field", "Previous Library", "Persistent", "Close Library"] {
|
||||
assert!(text.contains(item), "{item}: {text}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,6 +178,7 @@ pub fn show_icon_panel(app: &mut VectorcraftApp, ui: &mut Ui, id: &str) {
|
||||
links::ID => links::show(app, ui),
|
||||
asset_export::ID => asset_export::show(app, ui),
|
||||
css_properties::ID => css_properties::show(app, ui),
|
||||
id if library_panel::parse(id).is_some() => library_panel::show(app, ui, id),
|
||||
_ => {
|
||||
dim_label(ui, tl!("This panel is on the roadmap (see the parity plan)."));
|
||||
}
|
||||
@@ -228,6 +229,7 @@ pub fn panel_menu_items(app: &mut VectorcraftApp, ui: &mut Ui, id: &str) -> bool
|
||||
css_properties::ID => css_properties::menu(app, ui),
|
||||
variables::ID => variables::menu(app, ui),
|
||||
"actions" => actions::menu(app, ui),
|
||||
id if library_panel::parse(id).is_some() => library_panel::menu(app, ui, id),
|
||||
_ => return false,
|
||||
}
|
||||
true
|
||||
|
||||
@@ -22,8 +22,9 @@ use crate::theme::Tokens;
|
||||
use crate::widgets::{self, PanelDrag, SwatchRows, menu_item, swatch_tile};
|
||||
use crate::{VectorcraftApp, icons};
|
||||
|
||||
/// View modes of the swatch list (and of the library panel).
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
/// View modes of the swatch list (and of the library panels: `ui.libraryPanel {view}`).
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum View {
|
||||
SmallThumb,
|
||||
#[default]
|
||||
@@ -927,7 +928,7 @@ fn delete(app: &mut VectorcraftApp, names: Vec<String>, now: bool) {
|
||||
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 = app.ui.library_panel.as_ref().is_some_and(|o| o.kind == SwatchLibraries::KIND);
|
||||
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);
|
||||
@@ -1107,12 +1108,14 @@ pub(crate) fn limit_key(app: &VectorcraftApp, l: &str) -> Result<String, String>
|
||||
swatchlib::library(&app.session, l).map(|(info, _)| info.id).ok_or_else(|| format!("no swatch library `{l}` (see swatch.library.list)"))
|
||||
}
|
||||
|
||||
/// Swatch libraries in the library panel (engine: `swatch.library.*`).
|
||||
/// 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>;
|
||||
@@ -1124,6 +1127,12 @@ impl LibraryKind for SwatchLibraries {
|
||||
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>> {
|
||||
@@ -1189,15 +1198,13 @@ fn add_from_library(app: &mut VectorcraftApp, params: Value) {
|
||||
}
|
||||
}
|
||||
|
||||
/// `window.swatchLibrary {library}`: open a swatch library in the library panel (`library` null
|
||||
/// closes it).
|
||||
/// `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.list", |app, key| {
|
||||
swatchlib::library(&app.session, key).map(|(info, lib)| (info.id, info.name, lib.len()))
|
||||
})
|
||||
library_panel::open_command::<SwatchLibraries>(app, p)
|
||||
}
|
||||
|
||||
/// `swatch.library.load` params, then open the library in the panel.
|
||||
/// `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"] }))
|
||||
|
||||
@@ -8,7 +8,7 @@ use vectorcraft_doc::style_libs::STYLE_LIBRARIES;
|
||||
use vectorcraft_engine::Session;
|
||||
|
||||
use super::graphic_styles::GraphicStyleLibraries;
|
||||
use super::library_panel::{self, LibraryKind, OpenLibrary};
|
||||
use super::library_panel::{self, LibraryKind, LibraryPanel, last_open};
|
||||
use crate::VectorcraftApp;
|
||||
|
||||
fn app_with(services: crate::Services) -> VectorcraftApp {
|
||||
@@ -52,7 +52,7 @@ fn click(app: &mut VectorcraftApp, ctx: &egui::Context, name: &str, modifiers: M
|
||||
for t in [time, time + 0.25] {
|
||||
frame(ctx, vec![], Modifiers::NONE, t, |ui| library_panel::show_window(app, ui.ctx()));
|
||||
}
|
||||
let id = library_panel::tile_id::<GraphicStyleLibraries>(name);
|
||||
let id = library_panel::tile_id(&last_open(&app.ui).map(LibraryPanel::panel).unwrap_or_default(), name);
|
||||
let at = ctx.read_response(id).unwrap_or_else(|| panic!("no tile for {name}")).rect.center();
|
||||
let button = |pressed| Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers };
|
||||
frame(ctx, vec![Event::PointerMoved(at), button(true), button(false)], modifiers, time + 0.5, |ui| library_panel::show_window(app, ui.ctx()));
|
||||
@@ -69,13 +69,13 @@ fn clicking_a_library_style_adds_and_applies_it_in_one_step() {
|
||||
let rect = app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 80, "height": 40})).unwrap()["id"].as_u64().unwrap();
|
||||
let r = app.run("window.graphicStyleLibrary", json!({"library": "Shadows and Glows"})).unwrap();
|
||||
assert_eq!((r["open"].as_str(), r["name"].as_str()), (Some("shadows-glows"), Some("Shadows and Glows")));
|
||||
assert_eq!(app.ui.library_panel, Some(OpenLibrary { kind: "graphicStyles".into(), id: "shadows-glows".into() }));
|
||||
assert_eq!(last_open(&app.ui).map(|p| (p.kind.as_str(), p.id.as_str())), Some(("graphicStyles", "shadows-glows")));
|
||||
let undo = undo_len(&app);
|
||||
click(&mut app, &ctx, "Halo", Modifiers::NONE, 0.0);
|
||||
let (g, linked) = app.session.selection_graphic_style().expect("applied to the selection");
|
||||
assert_eq!((g.name.as_str(), linked), ("Halo", true));
|
||||
assert_eq!(undo_len(&app), undo + 1, "added and applied as one undo step");
|
||||
let tile = ctx.read_response(library_panel::tile_id::<GraphicStyleLibraries>("Halo")).unwrap().rect;
|
||||
let tile = ctx.read_response(library_panel::tile_id("library:graphicStyles:shadows-glows", "Halo")).unwrap().rect;
|
||||
assert_eq!(tile.width(), 30.0, "styles open in large thumbnails");
|
||||
// Alt-click adds the style's appearance on top, unlinked.
|
||||
let fills = |app: &VectorcraftApp| app.session.doc().unwrap().doc.node(vectorcraft_doc::NodeId(rect)).unwrap().appearance.items.len();
|
||||
@@ -98,7 +98,7 @@ fn clicking_a_library_style_adds_and_applies_it_in_one_step() {
|
||||
assert!(std::sync::Arc::ptr_eq(&lib, &GraphicStyleLibraries::get(&app, "shadows-glows").unwrap().1));
|
||||
assert!(app.run("window.graphicStyleLibrary", json!({"library": "nope"})).is_err());
|
||||
app.run("window.graphicStyleLibrary", json!({"library": null})).unwrap();
|
||||
assert!(app.ui.library_panel.is_none());
|
||||
assert!(last_open(&app.ui).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -153,7 +153,7 @@ fn save_dialog_writes_user_libraries_that_the_window_menu_lists() {
|
||||
assert!(app.ui.dialog.is_none());
|
||||
assert_eq!(user_items(&app), [("Warm".to_string(), "window.userGraphicStyleLibrary1".to_string())]);
|
||||
app.run("window.userGraphicStyleLibrary1", json!({})).unwrap();
|
||||
assert_eq!(app.ui.library_panel.as_ref().map(|o| o.id.as_str()), Some("user/Warm.vcstyles"));
|
||||
assert_eq!(last_open(&app.ui).map(|p| p.id.as_str()), Some("user/Warm.vcstyles"));
|
||||
let (name, lib) = GraphicStyleLibraries::get(&app, "user/Warm.vcstyles").unwrap();
|
||||
assert_eq!((name.as_str(), lib.len()), ("Warm", 1), "only the selected style");
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
@@ -179,7 +179,7 @@ fn saving_to_a_file_and_opening_a_vcstyles_file() {
|
||||
assert!(path.ends_with(".vcstyles"), "{path}");
|
||||
// Opening it opens it in the library panel, not as a document.
|
||||
crate::io::open_bytes(&mut app, "Saved.vcstyles", &bytes, None).unwrap();
|
||||
let open = app.ui.library_panel.clone().unwrap();
|
||||
let open = last_open(&app.ui).cloned().unwrap();
|
||||
assert_eq!(open.kind, GraphicStyleLibraries::KIND);
|
||||
assert_eq!(app.session.documents().len(), 1);
|
||||
let other = GraphicStyleLibraries::list(&app).into_iter().find(|l| l.id == open.id).unwrap();
|
||||
|
||||
@@ -394,9 +394,11 @@ pub struct UiState {
|
||||
/// Color Guide panel: variation kind, steps and amount (Color Guide Options).
|
||||
#[serde(default)]
|
||||
pub color_guide: vectorcraft_color::harmony::GuideOptions,
|
||||
/// The library open in the library panel (Window → Swatch Libraries).
|
||||
/// The library panels' settings (Window → Swatch Libraries, Graphic Style Libraries): of each
|
||||
/// library opened, and of the persistent ones, which reopen at startup, also once closed
|
||||
/// ([`crate::panels::library_panel`], `ui.libraryPanel`).
|
||||
#[serde(default)]
|
||||
pub library_panel: Option<crate::panels::library_panel::OpenLibrary>,
|
||||
pub library_panels: Vec<crate::panels::library_panel::LibraryPanel>,
|
||||
/// Flattener Preview panel: highlight, overprints, preset and options (`ui.flattenerPreview`).
|
||||
#[serde(default)]
|
||||
pub flattener_preview: crate::panels::flattener_preview::Settings,
|
||||
@@ -642,7 +644,7 @@ impl FloatingPanels {
|
||||
pub fn sanitize(groups: &mut Vec<FloatingPanels>, toolbar_pos: &mut Option<[f32; 2]>) {
|
||||
let mut seen = std::collections::BTreeSet::new();
|
||||
groups.retain_mut(|g| {
|
||||
g.panels.retain(|id| all_panels().any(|(p, _)| p == id) && seen.insert(id.clone()));
|
||||
g.panels.retain(|id| crate::menus::normalize_panel(id) == Some(id.as_str()) && seen.insert(id.clone()));
|
||||
g.active = g.active.min(g.panels.len().saturating_sub(1));
|
||||
!g.panels.is_empty()
|
||||
});
|
||||
@@ -769,7 +771,7 @@ impl Default for UiState {
|
||||
favorite_fonts: vec![],
|
||||
engine_prefs: Value::Null,
|
||||
color_guide: Default::default(),
|
||||
library_panel: None,
|
||||
library_panels: vec![],
|
||||
flattener_preview: Default::default(),
|
||||
color_guide_limit: String::new(),
|
||||
align_to: Default::default(),
|
||||
|
||||
@@ -319,6 +319,6 @@ fn the_popover_swatch_libraries_button_opens_the_libraries_menu() {
|
||||
let earth = menu_item_rect(&shapes, "Earth Tones").expect("the menu lists the built-in libraries");
|
||||
assert!(earth.top() > defaults.top());
|
||||
click(&mut app, &ctx, earth.center(), Modifiers::NONE, control_bar);
|
||||
let open = app.ui.library_panel.as_ref().map(|o| (o.kind.clone(), o.id.clone()));
|
||||
let open = crate::panels::library_panel::last_open(&app.ui).map(|o| (o.kind.clone(), o.id.clone()));
|
||||
assert_eq!(open, Some(("swatches".into(), "earth-tones".into())));
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ use serde_json::{Value, json};
|
||||
use vectorcraft_engine::Session;
|
||||
|
||||
use crate::panels::graphic_styles::GraphicStyleLibraries;
|
||||
use crate::panels::library_panel::library_menu;
|
||||
use crate::panels::library_panel::{self, library_menu};
|
||||
use crate::panels::swatches::SwatchLibraries;
|
||||
use crate::picks::{self, PickRequest};
|
||||
use crate::state::Dialog;
|
||||
@@ -232,10 +232,10 @@ fn other_swatch_library_asks_off_the_ui_thread() {
|
||||
click(&mut app, "Other Library…", library_menu::<SwatchLibraries>);
|
||||
assert!(matches!(shown.requests().as_slice(), [PickRequest::Open(_)]));
|
||||
poll(&mut app);
|
||||
assert!(app.ui.library_panel.is_none() && app.ui.status.is_empty(), "{}", app.ui.status);
|
||||
assert!(library_panel::last_open(&app.ui).is_none() && app.ui.status.is_empty(), "{}", app.ui.status);
|
||||
shown.answer(&[&file]);
|
||||
poll(&mut app);
|
||||
let open = app.ui.library_panel.clone().unwrap_or_else(|| panic!("no library open: {}", app.ui.status));
|
||||
let open = library_panel::last_open(&app.ui).cloned().unwrap_or_else(|| panic!("no library open: {}", app.ui.status));
|
||||
assert_eq!(open.kind, "swatches");
|
||||
let (_, lib) = vectorcraft_engine::cmd::swatchlib::library(&app.session, &open.id).unwrap();
|
||||
assert!(lib.swatch("Signal").is_some(), "{}", open.id);
|
||||
@@ -255,7 +255,7 @@ fn other_graphic_style_library_asks_off_the_ui_thread() {
|
||||
assert!(matches!(shown.requests().as_slice(), [PickRequest::Open(_)]));
|
||||
shown.answer(&[&file]);
|
||||
poll(&mut app);
|
||||
let open = app.ui.library_panel.clone().unwrap_or_else(|| panic!("no library open: {}", app.ui.status));
|
||||
let open = library_panel::last_open(&app.ui).cloned().unwrap_or_else(|| panic!("no library open: {}", app.ui.status));
|
||||
assert_eq!(open.kind, "graphicStyles");
|
||||
let (_, lib) = vectorcraft_engine::cmd::stylelib::library(&app.session, &open.id).unwrap();
|
||||
assert!(lib.style("Poster Look").is_some(), "{}", open.id);
|
||||
|
||||
@@ -168,7 +168,7 @@ fn a_dropped_swatch_library_opens_in_the_library_panel() {
|
||||
let gpl = temp_file("dropped.gpl", b"GIMP Palette\nName: Dropped\n#\n255 0 0\tRed\n 0 128 255\tSky\n");
|
||||
frame(&mut app, &ctx, vec![], &[&gpl], false);
|
||||
let id = format!("loaded/{gpl}");
|
||||
assert_eq!(app.ui.library_panel.as_ref().map(|o| o.id.as_str()), Some(id.as_str()), "{}", app.ui.status);
|
||||
assert_eq!(crate::panels::library_panel::last_open(&app.ui).map(|o| o.id.as_str()), Some(id.as_str()), "{}", app.ui.status);
|
||||
let lib = app.session.execute("swatch.library.get", &json!({"library": id})).unwrap();
|
||||
let names: Vec<&str> = lib["swatches"].as_array().unwrap().iter().filter_map(|w| w["name"].as_str()).collect();
|
||||
assert_eq!((lib["name"].as_str(), names), (Some("Dropped"), vec!["Red", "Sky"]));
|
||||
|
||||
+28
@@ -1399,6 +1399,34 @@ with); without `path` it returns `{data}`, and `user: true` saves into the user
|
||||
file, or another document's graphic styles, as a library to add from; a file in the user library folder, or a file
|
||||
with the same extension and bytes as one there, loads as that User Defined library.
|
||||
|
||||
## Library panels
|
||||
|
||||
Each swatch or graphic style library opens in a read-only panel of its own (#2603), as in Illustrator, and
|
||||
several can be open at once. `window.swatchLibrary {library}` and `window.graphicStyleLibrary {library}`
|
||||
(remote mode, `invoke_menu`) open one floating, or bring its panel forward when it is already open, and answer
|
||||
`{open, panel, name, count}`; `library: null` closes every panel of that kind. The panel id is
|
||||
`library:<kind>:<library id>` (`library:swatches:pastels`, `library:graphicStyles:hand-drawn`,
|
||||
`library:swatches:loaded/C:/Palettes/Brand.ase`), and the panel commands take it like any other panel's:
|
||||
`window.panel.float {panel, onto?, x?, y?}` groups or moves it, `window.panel.dock {panel}` docks it,
|
||||
`window.panel.close {panel}` closes it. `window.swatchLibrary.other {path}` and `window.graphicStyleLibrary.other
|
||||
{path}` load a file and open it the same way.
|
||||
|
||||
`ui.libraryPanel {panel, library?, view?, showFind?, persistent?}` sets one panel: `library` shows another library
|
||||
of its kind in that panel (what Previous Library and Next Library do; a library open in a panel of its own is
|
||||
brought forward instead), `view` is `smallThumb`, `mediumThumb`, `largeThumb`, `smallList` or `largeList`,
|
||||
`showFind` shows or hides the find field, and `persistent: true` is the panel menu's Persistent: the panel
|
||||
reopens when the app starts, with its view and find field, where the saved layout had it (or floating). A
|
||||
library loaded from a file is read again from its path; one whose file is gone is left out, and the status bar
|
||||
says which. A closed persistent panel can still be set, so `persistent: false` stops it reopening. The reply
|
||||
is `{panel, kind, library, view, showFind, persistent, open}`, and `ui.inspect` lists every panel's settings
|
||||
under `ui.library_panels`.
|
||||
|
||||
```json
|
||||
{"name":"invoke_menu","arguments":{"command":"window.swatchLibrary","params":{"library":"pastels"}}}
|
||||
{"name":"invoke_menu","arguments":{"command":"ui.libraryPanel","params":{"panel":"library:swatches:pastels","view":"smallList","persistent":true}}}
|
||||
{"name":"invoke_menu","arguments":{"command":"window.panel.close","params":{"panel":"library:swatches:pastels"}}}
|
||||
```
|
||||
|
||||
## Libraries
|
||||
|
||||
The Libraries panel's libraries hold graphics, colours and character and paragraph styles that any document can use.
|
||||
|
||||
+3
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user