mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-31 21:27:00 -06:00
+6
-5
File diff suppressed because one or more lines are too long
@@ -4,9 +4,9 @@
|
||||
//! The canvas is rasterized on the CPU; the GPU only composites it and draws the UI, so any
|
||||
//! adapter that can present to the window will do. [`adapter_order`] ranks the adapters: those that
|
||||
//! can't present to the window's surface never, then the one `WGPU_ADAPTER_NAME` names, hardware
|
||||
//! before software, the native backends (Vulkan, Metal, DX12) before OpenGL, and the power
|
||||
//! preference ([`power_preference`]) among the rest. eframe is handed [`selector`], which takes
|
||||
//! the first of that order.
|
||||
//! before software, the native backends (Vulkan, Metal, DX12) before OpenGL, the power preference
|
||||
//! ([`power_preference`]) among the rest, and a GPU's DX12 adapter before its Vulkan one (#545).
|
||||
//! eframe is handed [`selector`], which takes the first of that order.
|
||||
//!
|
||||
//! An adapter can report that it presents to the window and still fail once it does: on a hybrid
|
||||
//! Linux desktop under Wayland, the compositor runs on one GPU and may refuse the frame buffers
|
||||
@@ -89,15 +89,17 @@ fn key(backend: Backend, vendor: u32, device: u32) -> String {
|
||||
|
||||
/// The order to try `candidates` in (their indices): only those that present to the window and
|
||||
/// aren't in `skip`; the one whose name contains `named` (any case) first; hardware before
|
||||
/// software; native backends before OpenGL; then by `power`, keeping the system's order among
|
||||
/// equals (all of it for [`PowerPreference::None`]).
|
||||
/// software; native backends before OpenGL; then by `power`; then DX12 before Vulkan, keeping the
|
||||
/// system's order among equals (all of it for [`PowerPreference::None`]). Windows lists each GPU
|
||||
/// under both, and Intel's Vulkan driver made the whole window flicker black where DX12 and OpenGL
|
||||
/// didn't (#545); elsewhere there is no DX12 adapter, so the order is unchanged.
|
||||
pub fn adapter_order(candidates: &[Candidate], power: PowerPreference, named: Option<&str>, skip: &[String]) -> Vec<usize> {
|
||||
let named = named.map(str::to_lowercase).filter(|n| !n.is_empty());
|
||||
let mut order: Vec<(usize, &Candidate)> = candidates.iter().enumerate().filter(|(_, c)| c.presents && !skip.contains(&c.key)).collect();
|
||||
// Stable, so equals keep the system's order.
|
||||
order.sort_by_key(|(_, c)| {
|
||||
let unnamed = named.as_ref().is_some_and(|n| !c.name.to_lowercase().contains(n));
|
||||
(unnamed, c.device_type == DeviceType::Cpu, c.backend == Backend::Gl, power_rank(c.device_type, power))
|
||||
(unnamed, c.device_type == DeviceType::Cpu, c.backend == Backend::Gl, power_rank(c.device_type, power), c.backend == Backend::Vulkan)
|
||||
});
|
||||
order.into_iter().map(|(i, _)| i).collect()
|
||||
}
|
||||
@@ -361,7 +363,7 @@ mod tests {
|
||||
}
|
||||
|
||||
/// A Windows machine lists every GPU twice (Vulkan and DX12): power saving still takes the
|
||||
/// integrated GPU first, as wgpu's own choice did (#306).
|
||||
/// integrated GPU first, as wgpu's own choice did (#306), each GPU through DX12 first (#545).
|
||||
#[test]
|
||||
fn hybrid_laptop_on_windows_renders_on_the_integrated_gpu_with_power_saving() {
|
||||
let c = vec![
|
||||
@@ -372,8 +374,25 @@ mod tests {
|
||||
gpu("Microsoft Basic Render Driver", Backend::Dx12, DeviceType::Cpu),
|
||||
];
|
||||
let order = adapter_order(&c, PowerPreference::LowPower, None, &[]);
|
||||
assert_eq!(order, [1, 3, 0, 2, 4]);
|
||||
assert_eq!(adapter_order(&c, PowerPreference::HighPerformance, None, &[]), [0, 2, 1, 3, 4]);
|
||||
assert_eq!(order, [3, 1, 2, 0, 4]);
|
||||
assert_eq!(adapter_order(&c, PowerPreference::HighPerformance, None, &[]), [2, 0, 3, 1, 4]);
|
||||
}
|
||||
|
||||
/// The machine of #545: one Intel GPU, whose Vulkan driver made the window flicker black.
|
||||
/// DX12 comes first whatever the preference; Vulkan stays a fallback, and `WGPU_ADAPTER_NAME`
|
||||
/// or a restart leaving DX12 out still reach it.
|
||||
#[test]
|
||||
fn a_windows_gpu_renders_through_dx12_before_vulkan() {
|
||||
let c = vec![
|
||||
gpu("Intel(R) Graphics", Backend::Vulkan, DeviceType::IntegratedGpu),
|
||||
gpu("Intel(R) Graphics", Backend::Dx12, DeviceType::IntegratedGpu),
|
||||
gpu("Microsoft Basic Render Driver", Backend::Dx12, DeviceType::Cpu),
|
||||
gpu("Intel(R) Graphics", Backend::Gl, DeviceType::Other),
|
||||
];
|
||||
for power in [PowerPreference::LowPower, PowerPreference::HighPerformance, PowerPreference::None] {
|
||||
assert_eq!(adapter_order(&c, power, None, &[]), [1, 0, 3, 2], "{power:?}");
|
||||
}
|
||||
assert_eq!(adapter_order(&c, PowerPreference::LowPower, None, &[c[1].key.clone()]), [0, 3, 2], "DX12 failed: Vulkan next");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -174,6 +174,9 @@ pub fn anchor_buttons(app: &mut VectorcraftApp, ui: &mut Ui, controls: AnchorCon
|
||||
}
|
||||
}
|
||||
|
||||
/// The room the Control bar's inline X/Y/W/H fields need; with less, only the Transform link shows.
|
||||
const INLINE_TRANSFORM_WIDTH: f32 = 440.0;
|
||||
|
||||
/// The Control bar (Window → Control), context-sensitive like Illustrator's.
|
||||
pub fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
@@ -280,9 +283,13 @@ pub fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
}
|
||||
}
|
||||
ui.separator();
|
||||
// Transform fields.
|
||||
// The Transform link opens the whole Transform panel (reference point, rotate,
|
||||
// shear, options) in a popover; X/Y/W/H follow inline while the bar has room.
|
||||
crate::panels::transform::link(app, ui);
|
||||
// The bounding box, rotated with rotated objects: its centre and its own sides.
|
||||
if let Some(b) = app.selection_box() {
|
||||
if let Some(b) = app.selection_box()
|
||||
&& ui.available_width() >= INLINE_TRANSFORM_WIDTH
|
||||
{
|
||||
let c = b.center();
|
||||
let link = app.session.prefs.constrain_proportions;
|
||||
for (k, lbl, v) in [("x", "X:", c.x), ("y", "Y:", c.y), ("width", "W:", b.rect.width()), ("height", "H:", b.rect.height())] {
|
||||
|
||||
@@ -143,6 +143,7 @@
|
||||
Make Pixel Perfect Zarovnat na obrazové body
|
||||
Slice Řez
|
||||
Make Vytvořit
|
||||
Make and Expand Vytvořit a rozložit
|
||||
Release Uvolnit
|
||||
Create from Guides Vytvořit z vodítek
|
||||
Create from Selection Vytvořit z výběru
|
||||
@@ -199,8 +200,6 @@
|
||||
Merge Sloučit
|
||||
Gap Options… Možnosti mezer…
|
||||
Image Trace Vektorizace obrazu
|
||||
Make… Vytvořit…
|
||||
Make and Expand… Vytvořit a rozložit…
|
||||
Text Wrap Obtékání textu
|
||||
Text Wrap Options… Možnosti obtékání textu…
|
||||
Clipping Mask Ořezová maska
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1369,7 +1369,6 @@
|
||||
Make a theme on Create and save it. Crea un tema en Crear y guárdalo.
|
||||
Make all bleed settings the same Usar el mismo valor en todos los sangrados
|
||||
Make and Expand Crear y expandir
|
||||
Make and Expand… Crear y expandir…
|
||||
Make Clipping Mask Crear máscara de recorte
|
||||
Make Compound Shape Crear forma compuesta
|
||||
Make Envelope Crear envolvente
|
||||
@@ -1382,7 +1381,6 @@
|
||||
Make with Top Object Crear con objeto superior
|
||||
Make with Warp… Crear con deformación…
|
||||
Make/Release Clipping Mask Crear/soltar máscara de recorte
|
||||
Make… Crear…
|
||||
Manage Workspaces Administrar espacios de trabajo
|
||||
Manage Workspaces… Administrar espacios de trabajo…
|
||||
Manually Manualmente
|
||||
@@ -2546,6 +2544,7 @@
|
||||
Character: Carácter:
|
||||
Character options Opciones de carácter
|
||||
Stroke options Opciones de trazo
|
||||
Transform options Opciones de transformación
|
||||
Stroke Weight Grosor de trazo
|
||||
Stroke: Trazo:
|
||||
Stroke: click for swatches, Shift-click for the color mixer Trazo: clic para muestras, Shift+clic para el mezclador de colores
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1369,7 +1369,6 @@
|
||||
Make a theme on Create and save it. Crea un tema nella scheda Crea e salvalo.
|
||||
Make all bleed settings the same Usa lo stesso valore per tutte le abbondanze
|
||||
Make and Expand Crea ed espandi
|
||||
Make and Expand… Crea ed espandi…
|
||||
Make Clipping Mask Crea maschera di ritaglio
|
||||
Make Compound Shape Crea forma composta
|
||||
Make Envelope Crea involucro
|
||||
@@ -1382,7 +1381,6 @@
|
||||
Make with Top Object Crea con oggetto superiore
|
||||
Make with Warp… Crea con alterazione…
|
||||
Make/Release Clipping Mask Crea/rilascia maschera di ritaglio
|
||||
Make… Crea…
|
||||
Manage Workspaces Gestisci aree di lavoro
|
||||
Manage Workspaces… Gestisci aree di lavoro…
|
||||
Manually Manualmente
|
||||
@@ -2546,6 +2544,7 @@
|
||||
Character: Carattere:
|
||||
Character options Opzioni carattere
|
||||
Stroke options Opzioni traccia
|
||||
Transform options Opzioni di trasformazione
|
||||
Stroke Weight Spessore traccia
|
||||
Stroke: Traccia:
|
||||
Stroke: click for swatches, Shift-click for the color mixer Traccia: fai clic per i campioni, Shift+clic per il miscelatore colori
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -233,8 +233,6 @@
|
||||
Image Trace 画像トレース
|
||||
Image Tracing 画像トレースオブジェクト
|
||||
Tracing Presets トレースプリセット
|
||||
Make… 作成…
|
||||
Make and Expand… 作成して拡張…
|
||||
Text Wrap テキストの回り込み
|
||||
Text Wrap Options… テキストの回り込みオプション…
|
||||
Clipping Mask クリッピングマスク
|
||||
@@ -2352,6 +2350,7 @@
|
||||
Character: 文字:
|
||||
Character options 文字オプション
|
||||
Stroke options 線オプション
|
||||
Transform options 変形オプション
|
||||
Stroke: 線:
|
||||
Style スタイル
|
||||
Style Name: スタイル名:
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1242,13 +1242,11 @@
|
||||
Make a theme on Create and save it. Crie um tema em Criar e salve-o.
|
||||
Make all bleed settings the same Tornar todas as configurações de sangria iguais
|
||||
Make and Expand Criar e Expandir
|
||||
Make and Expand… Criar e Expandir…
|
||||
Make the bleed values differ Fazer os valores de sangria diferirem
|
||||
Make with Mesh… Criar com Malha…
|
||||
Make with Top Object Criar com Objeto Superior
|
||||
Make with Warp… Criar com Distorção…
|
||||
Make/Release Clipping Mask Criar/Liberar Máscara de Recorte
|
||||
Make… Criar…
|
||||
Manage Workspaces Gerenciar Espaços de Trabalho
|
||||
Manage Workspaces… Gerenciar Espaços de Trabalho…
|
||||
Manually Manualmente
|
||||
@@ -2248,6 +2246,7 @@
|
||||
Character: Caractere:
|
||||
Character options Opções de caractere
|
||||
Stroke options Opções de traçado
|
||||
Transform options Opções de transformação
|
||||
Stroke: Traçado:
|
||||
Style Estilo
|
||||
Style Name: Nome do Estilo:
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -1351,7 +1351,6 @@
|
||||
Make a theme on Create and save it. 在“创建”中制作主题并存储。
|
||||
Make all bleed settings the same 使所有出血设置相同
|
||||
Make and Expand 创建并扩展
|
||||
Make and Expand… 制作并展开…
|
||||
Make Clipping Mask 制作剪切蒙版
|
||||
Make Compound Shape 创建复合形状
|
||||
Make Envelope 制作封套
|
||||
@@ -1364,7 +1363,6 @@
|
||||
Make with Top Object 用顶层对象创建
|
||||
Make with Warp… 用变形创建…
|
||||
Make/Release Clipping Mask 创建/释放剪切蒙版
|
||||
Make… 制作…
|
||||
Manage Workspaces 管理工作区
|
||||
Manage Workspaces… 管理工作区…
|
||||
Manually 手动
|
||||
@@ -2484,6 +2482,7 @@
|
||||
Character: 字符:
|
||||
Character options 字符选项
|
||||
Stroke options 描边选项
|
||||
Transform options 变换选项
|
||||
Stroke Weight 描边粗细
|
||||
Stroke: 描边:
|
||||
Stroke: click for swatches, Shift-click for the color mixer 描边:单击开启色板,按住 Shift 单击开启混色器
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1387,7 +1387,6 @@
|
||||
Make a theme on Create and save it. 按一下「建立」即可製作主題並儲存。
|
||||
Make all bleed settings the same 將所有出血設定設為相同
|
||||
Make and Expand 製作並展開
|
||||
Make and Expand… 製作並展開…
|
||||
Make Clipping Mask 製作剪裁遮色片
|
||||
Make Compound Shape 製作複合形狀
|
||||
Make Envelope 製作封套
|
||||
@@ -1400,7 +1399,6 @@
|
||||
Make with Top Object 以最上層物件製作
|
||||
Make with Warp… 以彎曲製作…
|
||||
Make/Release Clipping Mask 製作/釋放剪裁遮色片
|
||||
Make… 製作…
|
||||
Manage Workspaces 管理工作區
|
||||
Manage Workspaces… 管理工作區…
|
||||
Manually 手動
|
||||
@@ -2566,6 +2564,7 @@
|
||||
Character: 字元:
|
||||
Character options 字元選項
|
||||
Stroke options 筆畫選項
|
||||
Transform options 變形選項
|
||||
Stroke Weight 筆畫寬度
|
||||
Stroke: 筆畫:
|
||||
Stroke: click for swatches, Shift-click for the color mixer 筆畫:按一下開啟色票,按住 Shift 按一下開啟混色器
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
+69
-25
@@ -2157,8 +2157,11 @@ pub fn menu_tree() -> Vec<(&'static str, Vec<Item>)> {
|
||||
sub(
|
||||
"Image Trace",
|
||||
vec![
|
||||
cp("Make…", "imageTrace.make", json!({"preset": "Black and White Logo"})),
|
||||
cp("Make and Expand…", "imageTrace.makeAndExpand", json!({"preset": "6 Colors"})),
|
||||
// Traced at once with the Default preset, as the Control bar's Image Trace
|
||||
// button does; other presets come from its arrow or the Image Trace panel
|
||||
// (#543: the "…" form asked for a preset's name in a text box).
|
||||
cp("Make", "imageTrace.make", json!({"preset": "Default"})),
|
||||
cp("Make and Expand", "imageTrace.makeAndExpand", json!({"preset": "Default"})),
|
||||
c("Release", "imageTrace.release"),
|
||||
c("Expand", "imageTrace.expand"),
|
||||
],
|
||||
@@ -2661,9 +2664,24 @@ pub fn ruler_menu_body(app: &VectorcraftApp, ui: &mut egui::Ui, clicked: &mut Op
|
||||
/// The in-window menu bar. Returns where its titles end (x): the bar itself takes the full width.
|
||||
pub fn menu_bar(app: &mut VectorcraftApp, ui: &mut egui::Ui) -> f32 {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let ctx = ui.ctx().clone();
|
||||
// The press that opens a menu takes the keyboard from a focused text field: the field and its
|
||||
// selection are kept while the menus are open, so their Select All, Cut, Copy and Paste act on
|
||||
// its text (#516), the frame after the click that chose them (that click takes the keyboard
|
||||
// from it again).
|
||||
type Field = (egui::Id, egui::text_edit::TextEditState);
|
||||
let (field_key, chosen_key) = (egui::Id::new("menu-bar-field"), egui::Id::new("menu-bar-field-event"));
|
||||
if let Some(((field, state), e)) = ctx.data(|d| d.get_temp::<(Field, egui::Event)>(chosen_key)) {
|
||||
ctx.data_mut(|d| d.remove::<(Field, egui::Event)>(chosen_key));
|
||||
state.store(&ctx, field);
|
||||
to_field(&ctx, field, e);
|
||||
}
|
||||
if let Some(field) = focused_text_field(&ctx).and_then(|id| Some((id, egui::TextEdit::load_state(&ctx, id)?))) {
|
||||
ctx.data_mut(|d| d.insert_temp::<Field>(field_key, field));
|
||||
}
|
||||
let mut clicked: Option<(String, Value)> = None;
|
||||
let tree = menu_tree();
|
||||
let end = egui::MenuBar::new()
|
||||
let (end, open) = egui::MenuBar::new()
|
||||
.ui(ui, |ui| {
|
||||
let mut titles = Vec::with_capacity(tree.len());
|
||||
for (i, (title, items)) in tree.iter().enumerate() {
|
||||
@@ -2674,12 +2692,21 @@ pub fn menu_bar(app: &mut VectorcraftApp, ui: &mut egui::Ui) -> f32 {
|
||||
};
|
||||
titles.push(ui.menu_button(text, |ui| menu_body(app, ui, items, &mut clicked)).response);
|
||||
}
|
||||
switch_on_hover(ui.ctx(), &titles);
|
||||
ui.cursor().min.x
|
||||
(ui.cursor().min.x, switch_on_hover(ui.ctx(), &titles))
|
||||
})
|
||||
.inner;
|
||||
let field = ctx.data(|d| d.get_temp::<Field>(field_key));
|
||||
if !open && focused_text_field(&ctx).is_none() {
|
||||
ctx.data_mut(|d| d.remove::<Field>(field_key));
|
||||
}
|
||||
if let Some((id, p)) = clicked {
|
||||
invoke(app, &id, p);
|
||||
match field.and_then(|f| Some((f, field_event(app, &id)?))) {
|
||||
Some(chosen) => {
|
||||
ctx.data_mut(|d| d.insert_temp(chosen_key, chosen));
|
||||
ctx.request_repaint();
|
||||
}
|
||||
None => invoke(app, &id, p),
|
||||
}
|
||||
}
|
||||
end
|
||||
}
|
||||
@@ -2692,29 +2719,25 @@ fn pointer_reaches_title(ctx: &egui::Context, title: &egui::Response, p: egui::P
|
||||
}
|
||||
|
||||
/// Like a native menu bar: while one top-level menu is open, moving the pointer onto another
|
||||
/// title opens that menu instead (egui alone needs a click on each title).
|
||||
fn switch_on_hover(ctx: &egui::Context, titles: &[egui::Response]) {
|
||||
/// title opens that menu instead (egui alone needs a click on each title). Returns whether a menu
|
||||
/// is open.
|
||||
fn switch_on_hover(ctx: &egui::Context, titles: &[egui::Response]) -> bool {
|
||||
let ids: Vec<egui::Id> = titles.iter().map(egui::Popup::default_response_id).collect();
|
||||
let Some(open) = ids.iter().position(|id| egui::Popup::is_id_open(ctx, *id)) else {
|
||||
return;
|
||||
return false;
|
||||
};
|
||||
// `Response::hovered` is false while a menu's popup is open, so hit-test the titles here.
|
||||
let Some(p) = ctx.pointer_hover_pos() else {
|
||||
return;
|
||||
};
|
||||
// Only a moving pointer switches: one resting on a title leaves the open menu alone.
|
||||
if ctx.input(|i| i.pointer.delta() == egui::Vec2::ZERO) {
|
||||
return;
|
||||
}
|
||||
let Some(i) = titles.iter().position(|title| pointer_reaches_title(ctx, title, p)) else {
|
||||
return;
|
||||
};
|
||||
if i != open
|
||||
let moving = ctx.input(|i| i.pointer.delta() != egui::Vec2::ZERO);
|
||||
if let Some(p) = ctx.pointer_hover_pos().filter(|_| moving)
|
||||
&& let Some(i) = titles.iter().position(|title| pointer_reaches_title(ctx, title, p))
|
||||
&& i != open
|
||||
&& let Some(id) = ids.get(i)
|
||||
{
|
||||
egui::Popup::open_id(ctx, *id);
|
||||
ctx.request_repaint();
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// A top-level menu's popup: as wide as its widest item (label plus shortcut), at least 230 pt;
|
||||
@@ -2859,13 +2882,21 @@ fn field_event(app: &mut VectorcraftApp, id: &str) -> Option<egui::Event> {
|
||||
/// Copy and Paste act on the field's text, as their keys do; everything else (and those commands
|
||||
/// with no field focused) goes to [`invoke`].
|
||||
pub fn invoke_from_system_menu(app: &mut VectorcraftApp, ctx: &egui::Context, id: &str, p: Value) {
|
||||
if ctx.text_edit_focused()
|
||||
&& let Some(e) = field_event(app, id)
|
||||
{
|
||||
ctx.input_mut(|i| i.events.push(e));
|
||||
return;
|
||||
match focused_text_field(ctx).and_then(|f| Some((f, field_event(app, id)?))) {
|
||||
Some((field, e)) => to_field(ctx, field, e),
|
||||
None => invoke(app, id, p),
|
||||
}
|
||||
invoke(app, id, p);
|
||||
}
|
||||
|
||||
/// The text field that has the keyboard, if any.
|
||||
fn focused_text_field(ctx: &egui::Context) -> Option<egui::Id> {
|
||||
ctx.memory(|m| m.focused()).filter(|&id| egui::TextEdit::load_state(ctx, id).is_some())
|
||||
}
|
||||
|
||||
/// Give text field `field` the keyboard (back) and event `e`, a [`field_event`], this frame.
|
||||
fn to_field(ctx: &egui::Context, field: egui::Id, e: egui::Event) {
|
||||
ctx.memory_mut(|m| m.request_focus(field));
|
||||
ctx.input_mut(|i| i.events.push(e));
|
||||
}
|
||||
|
||||
/// Invoke a menu/command id with UI side effects (dialogs for "…" commands that need input).
|
||||
@@ -3752,6 +3783,19 @@ mod tests {
|
||||
assert_eq!(pretty_shortcut(""), "");
|
||||
}
|
||||
|
||||
/// #543: Object › Image Trace › Make and Make and Expand trace with the Default preset at once,
|
||||
/// with no form asking for a preset's name.
|
||||
#[test]
|
||||
fn image_trace_make_traces_with_the_default_preset() {
|
||||
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({})).unwrap();
|
||||
for cmd in ["imageTrace.make", "imageTrace.makeAndExpand"] {
|
||||
let e = menu_entries(&app).into_iter().find(|e| e.command.as_deref() == Some(cmd)).unwrap();
|
||||
assert_eq!(e.path, ["Object", "Image Trace"]);
|
||||
assert_eq!(click_target(&e.label, cmd, &e.params), (cmd.to_string(), json!({"preset": "Default"})), "{}", e.label);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_appearance_is_an_object_menu_item_only() {
|
||||
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), crate::Services::default());
|
||||
|
||||
@@ -13,6 +13,8 @@ use crate::widgets::{self, menu_item};
|
||||
use crate::{VectorcraftApp, icons};
|
||||
|
||||
pub const ANGLE_PRESETS: [f64; 9] = [-180.0, -135.0, -90.0, -45.0, 0.0, 45.0, 90.0, 135.0, 180.0];
|
||||
/// The shear angle's presets: steep enough to be useful, short of the degenerate ±90°.
|
||||
pub const SHEAR_PRESETS: [f64; 9] = [-60.0, -45.0, -30.0, -15.0, 0.0, 15.0, 30.0, 45.0, 60.0];
|
||||
|
||||
/// Proportional size: the other dimension when one changes with the link on.
|
||||
pub fn constrained(w: f64, h: f64, new_w: Option<f64>, new_h: Option<f64>) -> (f64, f64) {
|
||||
@@ -151,8 +153,10 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
ui.add_space(4.0);
|
||||
let icon = icons::icon(ui, "dc-shear", 16.0, t.icon).on_hover_text(tl!("Shear"));
|
||||
crate::scrub::note_label(ui, icon.rect);
|
||||
if let Some(a) = widgets::plain_field(ui, "xfp-shear", 0.0, "°", 1, 56.0)
|
||||
// Shear is relative: the field rests at 0° and a new value shears by it.
|
||||
if let Some(a) = widgets::spin_plain(ui, "xfp-shear", 0.0, "°", 2, 96.0, 15.0, -89.0, &SHEAR_PRESETS)
|
||||
&& a != 0.0
|
||||
&& a.abs() < 90.0
|
||||
{
|
||||
app.run("object.shear", json!({"angle": a, "axis": "horizontal", "origin": origin})).ok();
|
||||
}
|
||||
@@ -206,6 +210,17 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The Control bar's underlined "Transform" link: a click toggles the Transform panel in a
|
||||
/// popover under it, as the Stroke link does with the Stroke panel.
|
||||
pub fn link(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let resp = ui.link(egui::RichText::new(tl!("Transform")).size(12.0).color(t.text).underline()).on_hover_text(tl!("Transform options"));
|
||||
widgets::popover(&resp, resp.clicked(), |ui| {
|
||||
ui.set_width(300.0);
|
||||
show(app, ui);
|
||||
});
|
||||
}
|
||||
|
||||
/// The link between W and H (Transform panel, Properties panel, Control bar): one toggle, the
|
||||
/// `constrainProportions` preference, which the size fields pass on as `proportional`.
|
||||
pub fn constrain_link(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
|
||||
@@ -242,3 +242,31 @@ fn the_window_menu_panel_keys() {
|
||||
app.ui.shortcut_overrides.clear();
|
||||
shortcut_editor::sync(&app.ui);
|
||||
}
|
||||
|
||||
/// Where `shapes` paint the text `s`.
|
||||
fn text_rect(shapes: &[Shape], s: &str) -> Option<Rect> {
|
||||
shapes.iter().find_map(|sh| match sh {
|
||||
Shape::Text(t) if t.galley.text() == s => Some(t.visual_bounding_rect()),
|
||||
Shape::Vec(v) => text_rect(v, s),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_transform_link_opens_the_transform_panel() {
|
||||
let mut app = app();
|
||||
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap();
|
||||
let ctx = context();
|
||||
frame(&mut app, &ctx, vec![], Modifiers::NONE, control_bar);
|
||||
let shapes = frame(&mut app, &ctx, vec![], Modifiers::NONE, control_bar);
|
||||
let link = text_rect(&shapes, "Transform").expect("the Control bar shows the Transform link");
|
||||
assert!(text_rect(&shapes, "Scale Corners").is_none());
|
||||
click(&mut app, &ctx, link.center(), Modifiers::NONE, control_bar);
|
||||
let shapes = frame(&mut app, &ctx, vec![], Modifiers::NONE, control_bar);
|
||||
for s in ["Scale Corners", "Scale Strokes & Effects", "0°"] {
|
||||
assert!(text_rect(&shapes, s).is_some(), "the popover shows {s}");
|
||||
}
|
||||
// A click outside closes it.
|
||||
click(&mut app, &ctx, Pos2::new(1300.0, 900.0), Modifiers::NONE, control_bar);
|
||||
assert!(text_rect(&frame(&mut app, &ctx, vec![], Modifiers::NONE, control_bar), "Scale Corners").is_none());
|
||||
}
|
||||
|
||||
@@ -171,3 +171,94 @@ fn cmd_a_on_the_canvas_selects_all_objects() {
|
||||
a.menu("select.all");
|
||||
assert_eq!(a.selected(), 2, "so does the menu's Select All");
|
||||
}
|
||||
|
||||
/// The copied texts of a frame's output.
|
||||
fn copied(out: &egui::FullOutput) -> Vec<String> {
|
||||
out.platform_output.commands.iter().filter_map(|c| if let egui::OutputCommand::CopyText(t) = c { Some(t.clone()) } else { None }).collect()
|
||||
}
|
||||
|
||||
/// #516: Ctrl/Cmd+C, X and V in a focused field (the window's own clipboard keys, which egui sends
|
||||
/// as Copy, Cut and Paste events) edit the field's text, never the art.
|
||||
#[test]
|
||||
fn the_clipboard_keys_in_a_focused_field_edit_its_text() {
|
||||
let mut a = App::new();
|
||||
a.app.run("window.panel", json!({"panel": "color"})).unwrap();
|
||||
for _ in 0..3 {
|
||||
a.frame(vec![]);
|
||||
}
|
||||
for text in ["37 pt", "FFFFFF"] {
|
||||
let (id, rect) = a.field(text);
|
||||
a.frame(vec![Event::PointerMoved(rect.center()), click(rect.center(), true)]);
|
||||
a.frame(vec![click(rect.center(), false)]);
|
||||
a.frame(vec![press(Key::A, Modifiers::COMMAND)]);
|
||||
assert!(a.ctx.memory(|m| m.has_focus(id)), "{text}: the field has the keyboard");
|
||||
let out = a.frame(vec![Event::Copy]);
|
||||
assert_eq!(copied(&out), [text], "Copy copies the field's text");
|
||||
assert!(a.app.session.clipboard.is_empty(), "{text}: no art copied");
|
||||
let out = a.frame(vec![Event::Cut]);
|
||||
assert_eq!(copied(&out), [text], "Cut cuts the field's text");
|
||||
a.frame(vec![Event::Paste("12".into())]);
|
||||
assert_eq!(a.ctx.data(|d| d.get_temp::<String>(id)).as_deref(), Some("12"), "Paste pastes into the field");
|
||||
assert_eq!(a.selected(), 1, "{text}: the art stays");
|
||||
a.frame(vec![press(Key::Escape, Modifiers::NONE)]);
|
||||
}
|
||||
}
|
||||
|
||||
/// The centre of the text `label` painted last (in the topmost layer) in a frame's output; with
|
||||
/// `title`, the first one painted (the menu bar's).
|
||||
fn text_at(out: &egui::FullOutput, label: &str, title: bool) -> Pos2 {
|
||||
let mut shapes: Box<dyn Iterator<Item = _>> = if title { Box::new(out.shapes.iter()) } else { Box::new(out.shapes.iter().rev()) };
|
||||
shapes
|
||||
.find_map(|s| match &s.shape {
|
||||
egui::Shape::Text(t) if t.galley.text().trim() == label => Some(Rect::from_min_size(t.pos, t.galley.size()).center()),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or_else(|| panic!("no {label:?} painted"))
|
||||
}
|
||||
|
||||
/// A click on `at` → the frame after it.
|
||||
fn click_at(a: &mut App, at: Pos2) -> egui::FullOutput {
|
||||
a.frame(vec![Event::PointerMoved(at), click(at, true)]);
|
||||
a.frame(vec![click(at, false)]);
|
||||
a.frame(vec![])
|
||||
}
|
||||
|
||||
/// The in-window menus' Select › All, Edit › Copy and Edit › Paste act on the text field that had the keyboard
|
||||
/// when the menu was opened (the click on the menu took it), and give it the keyboard back.
|
||||
#[test]
|
||||
fn the_in_window_edit_menu_acts_on_the_field_it_was_opened_from() {
|
||||
let mut a = App::new();
|
||||
// Text on the system clipboard.
|
||||
a.app.services.clipboard_read = Some(Box::new(|| Some("42 pt".into())));
|
||||
let (id, rect) = a.field("37 pt");
|
||||
a.frame(vec![Event::PointerMoved(rect.center()), click(rect.center(), true)]);
|
||||
a.frame(vec![click(rect.center(), false)]);
|
||||
let out = a.frame(vec![press(Key::End, Modifiers::NONE)]);
|
||||
let (edit, select) = (text_at(&out, "Edit", true), text_at(&out, "Select", true));
|
||||
let menu = |a: &mut App, title: Pos2, item: &str| {
|
||||
let out = click_at(a, title);
|
||||
assert!(!a.ctx.text_edit_focused(), "the menu took the keyboard");
|
||||
click_at(a, text_at(&out, item, false))
|
||||
};
|
||||
menu(&mut a, select, "All");
|
||||
assert!(a.ctx.memory(|m| m.has_focus(id)), "the field has the keyboard back");
|
||||
assert_eq!(a.field_selection(id), Some((0, 5)), "Select All selected the field's text");
|
||||
assert_eq!(a.selected(), 1, "not the art");
|
||||
a.frame(vec![press(Key::End, Modifiers::NONE)]);
|
||||
a.frame(vec![press(Key::A, Modifiers::COMMAND)]);
|
||||
a.frame(vec![]);
|
||||
let out = menu(&mut a, edit, "Copy");
|
||||
assert_eq!(copied(&out), ["37 pt"], "Copy copied the field's text");
|
||||
assert!(a.app.session.clipboard.is_empty(), "no art copied");
|
||||
// Art on the app's clipboard (Paste is enabled): the field still takes the text.
|
||||
a.app.run("edit.copy", json!({})).unwrap();
|
||||
a.frame(vec![press(Key::A, Modifiers::COMMAND)]);
|
||||
menu(&mut a, edit, "Paste");
|
||||
assert_eq!(a.ctx.data(|d| d.get_temp::<String>(id)).as_deref(), Some("42 pt"), "Paste pasted into the field");
|
||||
assert_eq!(a.selected(), 1, "nothing pasted on the canvas");
|
||||
// With no field focused, the menu's Select All selects the art.
|
||||
a.frame(vec![press(Key::Escape, Modifiers::NONE)]);
|
||||
a.frame(vec![]);
|
||||
menu(&mut a, select, "All");
|
||||
assert_eq!(a.selected(), 2);
|
||||
}
|
||||
|
||||
@@ -806,18 +806,23 @@ pub fn opacity_blend(
|
||||
edit
|
||||
}
|
||||
|
||||
/// The 3×3 reference point locator. Returns a new index when clicked.
|
||||
/// The 3×3 reference point locator: nine squares, the eight outer ones joined by a line around
|
||||
/// the box (the centre one stands alone), the current one filled. Returns a new index when clicked.
|
||||
pub fn reference_point(ui: &mut Ui, current: usize) -> Option<usize> {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let size = 22.0;
|
||||
// 5 pt squares with 4 pt gaps; the origin is snapped to whole points so the 1 pt lines through
|
||||
// the squares' centres stay crisp.
|
||||
let (sq, step) = (5.0, 9.0);
|
||||
let size = 2.0 * step + sq;
|
||||
let (rect, _) = ui.allocate_exact_size(Vec2::splat(size), Sense::hover());
|
||||
let o = rect.min.round();
|
||||
let centre = |i: usize| o + Vec2::new(sq / 2.0 + (i % 3) as f32 * step, sq / 2.0 + (i / 3) as f32 * step);
|
||||
let line = Stroke::new(1.0, t.text_dim);
|
||||
ui.painter().rect_stroke(Rect::from_two_pos(centre(0), centre(8)), 0.0, line, StrokeKind::Middle);
|
||||
let mut out = None;
|
||||
let step = size / 2.0 - 2.5;
|
||||
ui.painter().rect_stroke(rect.shrink(4.5), 0.0, Stroke::new(1.0, t.text_dim), StrokeKind::Middle);
|
||||
for i in 0..9 {
|
||||
let c = Pos2::new(rect.left() + 3.0 + (i % 3) as f32 * step + 1.5, rect.top() + 3.0 + (i / 3) as f32 * step + 1.5);
|
||||
let r = Rect::from_center_size(c, Vec2::splat(5.0));
|
||||
let resp = ui.interact(r.expand(1.5), ui.id().with(("refpt", i)), Sense::click());
|
||||
let r = Rect::from_center_size(centre(i), Vec2::splat(sq));
|
||||
let resp = ui.interact(r.expand(2.0), ui.id().with(("refpt", i)), Sense::click());
|
||||
if resp.clicked() {
|
||||
out = Some(i);
|
||||
}
|
||||
@@ -825,7 +830,7 @@ pub fn reference_point(ui: &mut Ui, current: usize) -> Option<usize> {
|
||||
ui.painter().rect_filled(r, 0.0, t.text);
|
||||
} else {
|
||||
ui.painter().rect_filled(r, 0.0, t.panel);
|
||||
ui.painter().rect_stroke(r, 0.0, Stroke::new(1.0, t.text_dim), StrokeKind::Inside);
|
||||
ui.painter().rect_stroke(r, 0.0, Stroke::new(1.0, if resp.hovered() { t.text } else { t.text_dim }), StrokeKind::Inside);
|
||||
}
|
||||
}
|
||||
out
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ How the web shell (`apps/vectorcraft-web/src/web.rs`) differs from desktop:
|
||||
|
||||
The canvas is rasterized on the CPU (`vectorcraft-render`, vello_cpu); the GPU (wgpu, through eframe) only composites the canvas texture and draws the UI, so any GPU that can show the window will do. The desktop app picks the window's adapter itself (`apps/vectorcraft/src/gpu.rs`, eframe's `native_adapter_selector`) and logs every adapter it found and the one it uses.
|
||||
|
||||
- **Order:** adapters that report they can't present to the window are never tried; then the one `WGPU_ADAPTER_NAME` names; hardware before software (llvmpipe, WARP); the native backends (Vulkan, Metal, DX12) before OpenGL; then the power preference, keeping the system's order among equals.
|
||||
- **Order:** adapters that report they can't present to the window are never tried; then the one `WGPU_ADAPTER_NAME` names; hardware before software (llvmpipe, WARP); the native backends (Vulkan, Metal, DX12) before OpenGL; then the power preference; then, on Windows, where each GPU is listed under both, a GPU's DX12 adapter before its Vulkan one (Intel's Vulkan driver made the whole window flicker black, #545), keeping the system's order among equals. `WGPU_BACKEND=vulkan` still picks Vulkan.
|
||||
- **Preferences › Performance › Graphics Processor** (`gpuPreference`): `automatic` (the default), `lowPower` (Power Saving, the integrated GPU) or `highPerformance` (the discrete GPU). The adapter is chosen when the window opens, so a change applies after a restart.
|
||||
- **Automatic on Windows and macOS** is power saving: the system shows frames from any GPU, and presenting frames rendered on a discrete GPU through the integrated one made the window flicker on some hybrid laptops (#306).
|
||||
- **Automatic on Linux and the BSDs** keeps the system's order: Mesa's Vulkan device-select layer puts the GPU the desktop runs on first (the integrated one on hybrid laptops; `DRI_PRIME` and `MESA_VK_DEVICE_SELECT` steer it). A Wayland compositor may not accept frames from another GPU: on a desktop whose compositor ran on an NVIDIA GPU, rendering on the Ryzen's integrated GPU made KWin end the window's connection ("importing the supplied dmabufs failed") and 0.5.0 crashed at startup (#502).
|
||||
|
||||
Reference in New Issue
Block a user