Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61d372bb7f | ||
|
|
82dc5f8182 | ||
|
|
79c81d754c | ||
|
|
97318c188b | ||
|
|
94141eca82 | ||
|
|
5df4ddd4b6 | ||
|
|
021a9474f8 | ||
|
|
de564bc24f | ||
|
|
495694e1ac |
No files matched your search
@@ -31,7 +31,7 @@ use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, OnceLock};
|
||||
|
||||
use eframe::egui_wgpu::NativeAdapterSelectorMethod;
|
||||
use eframe::wgpu::{self, Backend, DeviceType, PowerPreference};
|
||||
use eframe::wgpu::{self, Backend, Backends, DeviceType, PowerPreference};
|
||||
|
||||
/// The adapters a restart leaves out, those the window failed on, as comma-separated [`key`]s
|
||||
/// (`Vulkan:1002:164e`, `Metal:Intel Iris Pro Graphics`). Set by the app itself when it starts
|
||||
@@ -81,6 +81,15 @@ pub fn preferred_displays(pref: Option<&str>, env: Option<PowerPreference>) -> V
|
||||
if automatic(pref, env) { display_gpus() } else { Vec::new() }
|
||||
}
|
||||
|
||||
/// The graphics backends the window starts with: on Windows Direct3D 12, falling back to OpenGL,
|
||||
/// unless `WGPU_BACKEND` names others (`env_set`; `default` then holds them). Creating a Vulkan
|
||||
/// instance loads every installed Vulkan driver into the process, and a faulty one (Intel's
|
||||
/// `igvk64.dll`, #806) crashed the app before its window appeared. DX12 is Windows' own backend,
|
||||
/// as in PdfCraft. Elsewhere, `default`.
|
||||
pub fn backends(default: Backends, env_set: bool) -> Backends {
|
||||
if cfg!(windows) && !env_set { Backends::DX12 | Backends::GL } else { default }
|
||||
}
|
||||
|
||||
/// One adapter, as [`adapter_order`] sees it.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Candidate {
|
||||
@@ -809,6 +818,18 @@ mod tests {
|
||||
assert_eq!(adapter_order(&c, PowerPreference::LowPower, &[], None, &[c[1].key.clone()]), [0, 3, 2], "DX12 failed: Vulkan next");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_never_loads_vulkan_drivers_unless_asked() {
|
||||
let all = Backends::all();
|
||||
let start = backends(all, false);
|
||||
if cfg!(windows) {
|
||||
assert_eq!(start, Backends::DX12 | Backends::GL, "#806");
|
||||
} else {
|
||||
assert_eq!(start, all);
|
||||
}
|
||||
assert_eq!(backends(Backends::VULKAN, true), Backends::VULKAN, "WGPU_BACKEND=vulkan still picks it");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn power_preference_follows_the_environment_then_the_preference() {
|
||||
assert_eq!(power_preference(None, None), AUTOMATIC);
|
||||
|
||||
@@ -467,6 +467,7 @@ fn main() -> std::process::ExitCode {
|
||||
// Metal on an Iris Pro, #651). `WGPU_VALIDATION_INDIRECT_CALL=1` turns it back on.
|
||||
create.instance_descriptor.flags =
|
||||
(eframe::wgpu::InstanceFlags::from_build_config() - eframe::wgpu::InstanceFlags::VALIDATION_INDIRECT_CALL).with_env();
|
||||
create.instance_descriptor.backends = gpu::backends(create.instance_descriptor.backends, std::env::var_os("WGPU_BACKEND").is_some());
|
||||
}
|
||||
options
|
||||
};
|
||||
|
||||
@@ -142,6 +142,17 @@ fn de_key<'de, T: Keyed, D: Deserializer<'de>>(d: D) -> std::result::Result<T, D
|
||||
T::parse(&s).ok_or_else(|| D::Error::custom(format!("`{s}`: {}", T::KEYS)))
|
||||
}
|
||||
|
||||
/// A size in pixels: a number, rounded to whole pixels (`300.0` is 300).
|
||||
fn de_pixels<'de, D: Deserializer<'de>>(d: D) -> std::result::Result<Option<u32>, D::Error> {
|
||||
let Some(v) = Option::<f64>::deserialize(d)? else { return Ok(None) };
|
||||
let px = v.round();
|
||||
if !(px.is_finite() && px >= 0.0 && px <= f64::from(u32::MAX)) {
|
||||
return Err(D::Error::custom(format!("{v} is not a size in pixels")));
|
||||
}
|
||||
// In range and whole: the cast is exact.
|
||||
Ok(Some(px as u32))
|
||||
}
|
||||
|
||||
/// The file format.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -361,8 +372,10 @@ pub struct WebSettings {
|
||||
/// JPEG: embed the sRGB profile.
|
||||
pub embed_profile: bool,
|
||||
/// The image width in pixels (the height follows).
|
||||
#[serde(deserialize_with = "de_pixels")]
|
||||
pub width: Option<u32>,
|
||||
/// The image height in pixels (the width follows).
|
||||
#[serde(deserialize_with = "de_pixels")]
|
||||
pub height: Option<u32>,
|
||||
/// The image size in percent of the art's size in points.
|
||||
pub percent: Option<f64>,
|
||||
|
||||
@@ -285,9 +285,16 @@ fn image_size_region_and_metadata() {
|
||||
assert_eq!(size(&mut s, json!({"width": 80})), (80, 60));
|
||||
assert_eq!(size(&mut s, json!({"height": 15})), (20, 15));
|
||||
assert_eq!(size(&mut s, json!({"percent": 150})), (60, 45));
|
||||
// The dialog's fields send decimal numbers: rounded to whole pixels.
|
||||
assert_eq!(size(&mut s, json!({"width": 80.0})), (80, 60));
|
||||
assert_eq!(size(&mut s, json!({"height": 14.6})), (20, 15));
|
||||
for bad in [
|
||||
json!({"percent": 0}),
|
||||
json!({"width": 0}),
|
||||
json!({"width": 0.2}),
|
||||
json!({"width": -5}),
|
||||
json!({"height": 1e300}),
|
||||
json!({"height": "tall"}),
|
||||
json!({"format": "bmp"}),
|
||||
json!({"colors": "many"}),
|
||||
json!({"matte": "plaid"}),
|
||||
|
||||
@@ -936,6 +936,14 @@ fn prune_unseen(frame: &Frame<'_>, view: &mut PageView, doc: &mut Document) -> u
|
||||
return 0;
|
||||
}
|
||||
let mut with = view.difference(doc, rect).any;
|
||||
// Taking a text object out changes only what is drawn in its box. When the art would still
|
||||
// differ from the page by more than `compare` allows with every candidate gone, the layers
|
||||
// aren't used whatever is pruned: don't draw them again for each one (#758).
|
||||
let margin = OUTLINE_REACH + 1.0;
|
||||
let boxes: f64 = found.iter().filter_map(|(_, b)| *b).map(|b| b.inflate(margin, margin).intersect(rect).area().max(0.0)).sum();
|
||||
if with - boxes / rect.area().max(1e-9) > MAX_DIFFERENCE {
|
||||
return 0;
|
||||
}
|
||||
let mut removed = 0;
|
||||
for (id, bounds) in found {
|
||||
if bounds.is_none_or(|b| b.intersect(rect).is_zero_area()) {
|
||||
|
||||
@@ -346,7 +346,10 @@ impl Tool for ArtboardTool {
|
||||
for h in Handle::ALL {
|
||||
o.push(Overlay::Anchor { p: h.pos(r), color: BLUE, filled: false, size: 7.0 });
|
||||
}
|
||||
o.push(Overlay::Label { p: Point::new(r.x0, r.y0 - cx.tol(14.0)), text: format!("{:02} - {}", self.active + 1, ab.name), color: BLUE });
|
||||
// Where the canvas names the other artboards: on the top edge, flush with the left one
|
||||
// (labels are drawn 8 px right of and 14 px above their point).
|
||||
let at = Point::new(r.x0 - cx.tol(8.0), r.y0 - cx.tol(4.0));
|
||||
o.push(Overlay::Label { p: at, text: format!("{:02} - {}", self.active + 1, ab.name), color: BLUE });
|
||||
if let (Some(Drag::Resize { began: true, .. }), true) = (self.drag, cx.measurement_labels) {
|
||||
o.push(Overlay::Measure {
|
||||
p: Point::new(r.x1, r.y1) + Vec2::new(cx.tol(12.0), cx.tol(12.0)),
|
||||
|
||||
@@ -349,8 +349,11 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
if app.ui.view.pixel_preview && v.zoom >= PIXEL_GRID_ZOOM && app.session.prefs.show_pixel_grid {
|
||||
grid(&painter, &xf, 1.0, 1, LineLook { color: PIXEL_GRID, dots: false });
|
||||
}
|
||||
// Artboard edges and names.
|
||||
// Artboard edges and names. The Artboard tool labels its active artboard itself (following
|
||||
// a move or resize): that one isn't named twice.
|
||||
let active_ab = 0;
|
||||
let artboard_tool = app.session.tool_id() == "artboard";
|
||||
let tool_labelled = artboard_tool.then(|| app.session.tool_options()["active"].as_u64()).flatten();
|
||||
for (i, ab) in doc.artboards.iter().enumerate() {
|
||||
let r = xf.rect_to_screen(ab.rect);
|
||||
let c = if i == active_ab { Color32::from_gray(0) } else { Color32::from_gray(120) };
|
||||
@@ -359,7 +362,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
if doc.setup.has_bleed() {
|
||||
painter.add(Shape::closed_line(xf.quad(doc.setup.bleed_rect(ab.rect)), Stroke::new(1.0, t.bleed)));
|
||||
}
|
||||
if app.session.tool_id() == "artboard" || doc.artboards.len() > 1 {
|
||||
if (artboard_tool || doc.artboards.len() > 1) && tool_labelled != u64::try_from(i).ok() {
|
||||
painter.text(
|
||||
r.left_top() - vec2(0.0, 4.0),
|
||||
egui::Align2::LEFT_BOTTOM,
|
||||
@@ -1078,47 +1081,46 @@ fn rulers(ui: &Ui, full: egui::Rect, xf: &Xf, hover: Option<Point>, unit: Unit,
|
||||
let step = unit.ruler_step(xf.zoom);
|
||||
let minor = step / 10.0;
|
||||
let font = egui::FontId::proportional(9.5);
|
||||
let a = xf.to_doc(top.left_top());
|
||||
let b = xf.to_doc(top.right_top());
|
||||
let clip_top = p.with_clip_rect(top);
|
||||
let mut x = (a.x / per / minor).floor() * minor;
|
||||
while x * per <= b.x {
|
||||
let sx = xf.to_screen(Point::new(x * per, 0.0)).x;
|
||||
let is_major = ((x / step).round() * step - x).abs() < minor * 0.01;
|
||||
let is_mid = ((x / (step / 2.0)).round() * (step / 2.0) - x).abs() < minor * 0.01;
|
||||
let len = if is_major {
|
||||
let near = |v: f64, every: f64| ((v / every).round() * every - v).abs() < minor * 0.01;
|
||||
// A tick's length: the labelled ones longest, the halves between them shorter.
|
||||
let tick = |v: f64| {
|
||||
if near(v, step) {
|
||||
RULER
|
||||
} else if is_mid {
|
||||
} else if near(v, step / 2.0) {
|
||||
7.0
|
||||
} else {
|
||||
4.0
|
||||
};
|
||||
clip_top.line_segment([pos2(sx, top.bottom() - len), pos2(sx, top.bottom())], Stroke::new(1.0, t.ruler_tick));
|
||||
if is_major {
|
||||
}
|
||||
};
|
||||
// Each ruler measures along its own edge, in the canvas's coordinates turned with the view
|
||||
// (#826): at 0° the top one reads x and the left one y. `draw` gets each tick's distance
|
||||
// (screen points) from `from` along the ruler, and its value in `unit`.
|
||||
let ticks = |from: Pos2, to: Pos2, axis: vectorcraft_geom::Vec2, draw: &mut dyn FnMut(f32, f64)| {
|
||||
let along = |s: Pos2| xf.to_doc(s).to_vec2().dot(axis);
|
||||
let (a, b) = (along(from), along(to));
|
||||
let mut v = (a / per / minor).floor() * minor;
|
||||
while v * per <= b {
|
||||
draw(((v * per - a) * xf.zoom) as f32, v);
|
||||
v += minor;
|
||||
}
|
||||
};
|
||||
// The canvas's directions along the screen's x and y.
|
||||
let (sn, cs) = xf.rot.sin_cos();
|
||||
let clip_top = p.with_clip_rect(top);
|
||||
ticks(top.left_top(), top.right_top(), vectorcraft_geom::Vec2::new(cs, -sn), &mut |d, x| {
|
||||
let sx = top.left() + d;
|
||||
clip_top.line_segment([pos2(sx, top.bottom() - tick(x)), pos2(sx, top.bottom())], Stroke::new(1.0, t.ruler_tick));
|
||||
if near(x, step) {
|
||||
clip_top.text(pos2(sx + 2.0, top.top() + 1.0), egui::Align2::LEFT_TOP, ruler_label(x, step), font.clone(), t.ruler_tick);
|
||||
}
|
||||
x += minor;
|
||||
}
|
||||
let a = xf.to_doc(left.left_top());
|
||||
let b = xf.to_doc(left.left_bottom());
|
||||
});
|
||||
let clip_left = p.with_clip_rect(left);
|
||||
let mut y = (a.y / per / minor).floor() * minor;
|
||||
while y * per <= b.y {
|
||||
let sy = xf.to_screen(Point::new(0.0, y * per)).y;
|
||||
let is_major = ((y / step).round() * step - y).abs() < minor * 0.01;
|
||||
let is_mid = ((y / (step / 2.0)).round() * (step / 2.0) - y).abs() < minor * 0.01;
|
||||
let len = if is_major {
|
||||
RULER
|
||||
} else if is_mid {
|
||||
7.0
|
||||
} else {
|
||||
4.0
|
||||
};
|
||||
clip_left.line_segment([pos2(left.right() - len, sy), pos2(left.right(), sy)], Stroke::new(1.0, t.ruler_tick));
|
||||
if is_major {
|
||||
ticks(left.left_top(), left.left_bottom(), vectorcraft_geom::Vec2::new(sn, cs), &mut |d, y| {
|
||||
let sy = left.top() + d;
|
||||
clip_left.line_segment([pos2(left.right() - tick(y), sy), pos2(left.right(), sy)], Stroke::new(1.0, t.ruler_tick));
|
||||
if near(y, step) {
|
||||
// Vertical labels read top-to-bottom, one digit per line like Illustrator.
|
||||
let s = ruler_label(y, step);
|
||||
for (k, ch) in s.chars().enumerate() {
|
||||
for (k, ch) in ruler_label(y, step).chars().enumerate() {
|
||||
clip_left.text(
|
||||
pos2(left.left() + 4.0, sy + 2.0 + k as f32 * 8.5),
|
||||
egui::Align2::LEFT_TOP,
|
||||
@@ -1128,8 +1130,7 @@ fn rulers(ui: &Ui, full: egui::Rect, xf: &Xf, hover: Option<Point>, unit: Unit,
|
||||
);
|
||||
}
|
||||
}
|
||||
y += minor;
|
||||
}
|
||||
});
|
||||
if let Some(h) = hover {
|
||||
let s = xf.to_screen(h);
|
||||
clip_top.line_segment([pos2(s.x, top.top()), pos2(s.x, top.bottom())], Stroke::new(1.0, t.text));
|
||||
@@ -2050,6 +2051,47 @@ mod tests {
|
||||
use vectorcraft_engine::Session;
|
||||
use vectorcraft_geom::Shape as _;
|
||||
|
||||
/// #826: in a turned view each ruler still has ticks along its whole length; at 0° the top one
|
||||
/// reads x where the canvas has it.
|
||||
#[test]
|
||||
fn rulers_cover_their_length_at_any_rotation() {
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
let full = egui::Rect::from_min_size(pos2(0.0, 0.0), vec2(800.0, 600.0));
|
||||
let [top, left, _] = ruler_rects(full);
|
||||
for deg in [0.0_f64, 30.0, 90.0, 135.0, 200.0] {
|
||||
let xf = Xf { rect: full, zoom: 1.0, center: Point::new(300.0, 200.0), rot: deg.to_radians() };
|
||||
let mut out = ctx.run_ui(Default::default(), |ui| rulers(ui, full, &xf, None, Unit::Points, &Tokens::get(ui.ctx())));
|
||||
out.textures_delta.clear();
|
||||
// The ticks' positions along each ruler.
|
||||
let along = |ruler: egui::Rect, x: bool| -> Vec<f32> {
|
||||
let mut at: Vec<f32> = out
|
||||
.shapes
|
||||
.iter()
|
||||
.filter(|c| c.clip_rect == ruler)
|
||||
.filter_map(|c| match &c.shape {
|
||||
Shape::LineSegment { points: [a, b], .. } if x && a.x == b.x => Some(a.x),
|
||||
Shape::LineSegment { points: [a, b], .. } if !x && a.y == b.y => Some(a.y),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
at.sort_by(f32::total_cmp);
|
||||
at
|
||||
};
|
||||
for (ruler, x) in [(top, true), (left, false)] {
|
||||
let at = along(ruler, x);
|
||||
let (start, end) = if x { (ruler.left(), ruler.right()) } else { (ruler.top(), ruler.bottom()) };
|
||||
let (Some(first), Some(last)) = (at.first(), at.last()) else { panic!("{deg}°: no ticks") };
|
||||
assert!(first - start < 20.0 && end - last < 20.0, "{deg}°: ticks from {first} to {last} on {start}..{end}");
|
||||
assert!(at.windows(2).all(|w| w[1] - w[0] < 20.0), "{deg}°: a gap in the ticks");
|
||||
}
|
||||
if deg == 0.0 {
|
||||
let origin = xf.to_screen(Point::ZERO).x;
|
||||
assert!(along(top, true).iter().any(|x| (x - origin).abs() < 0.01), "a tick at x = 0");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shaper_selection_highlights_only_the_visible_result() {
|
||||
let mut s = Session::new();
|
||||
|
||||
@@ -265,23 +265,7 @@ pub fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Align buttons.
|
||||
for (icon, tip, p) in [
|
||||
("align-start-vertical", "Horizontal Align Left", json!({"horizontal": "left"})),
|
||||
("align-center-vertical", "Horizontal Align Center", json!({"horizontal": "center"})),
|
||||
("align-end-vertical", "Horizontal Align Right", json!({"horizontal": "right"})),
|
||||
("align-start-horizontal", "Vertical Align Top", json!({"vertical": "top"})),
|
||||
("align-center-horizontal", "Vertical Align Center", json!({"vertical": "center"})),
|
||||
("align-end-horizontal", "Vertical Align Bottom", json!({"vertical": "bottom"})),
|
||||
] {
|
||||
if widgets::icon_button(ui, icon, tl!(tip), false, 24.0).clicked() {
|
||||
let mut p = p;
|
||||
if sel.len() == 1 {
|
||||
p["to"] = json!("artboard");
|
||||
}
|
||||
app.run("object.align", p).ok();
|
||||
}
|
||||
}
|
||||
crate::panels::align::align_buttons(app, ui, 24.0);
|
||||
ui.separator();
|
||||
// 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.
|
||||
|
||||
@@ -90,12 +90,19 @@ fn show(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
let screen =
|
||||
d.fields.get("screen").and_then(Value::as_array).and_then(|a| Some(egui::pos2(a.first()?.as_f64()? as f32, a.get(1)?.as_f64()? as f32)));
|
||||
let chip = Point::new(d.f64("x", 0.0), d.f64("y", 0.0));
|
||||
let pos = screen.unwrap_or_else(|| {
|
||||
app.canvas_rect.zip(app.view().copied()).map_or(ctx.content_rect().center(), |(r, v)| Xf::new(r, &v).to_screen(chip) + vec2(14.0, 14.0))
|
||||
let area = egui::Id::new("gradient-stop-popover");
|
||||
let size = ctx.memory(|m| m.area_rect(area)).map_or(POPOVER_SIZE, |r| r.size());
|
||||
let pos = screen.unwrap_or_else(|| match app.canvas_rect.zip(app.view().copied()) {
|
||||
Some((canvas, v)) => {
|
||||
let xf = Xf::new(canvas, &v);
|
||||
let at = xf.to_screen(chip);
|
||||
selection_on_screen(app, &xf).map_or(at + vec2(14.0, 14.0), |art| beside_art(at, art, size, canvas))
|
||||
}
|
||||
None => ctx.content_rect().center(),
|
||||
});
|
||||
let swatches = d.str("tab") == "swatches";
|
||||
let mut tab = None;
|
||||
let resp = egui::Area::new(egui::Id::new("gradient-stop-popover"))
|
||||
let resp = egui::Area::new(area)
|
||||
.order(egui::Order::Foreground)
|
||||
.fixed_pos(pos)
|
||||
.constrain(true)
|
||||
@@ -150,6 +157,34 @@ fn show(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The popover's size before it has been drawn.
|
||||
const POPOVER_SIZE: egui::Vec2 = vec2(272.0, 360.0);
|
||||
|
||||
/// The selected art's box on the screen.
|
||||
fn selection_on_screen(app: &VectorcraftApp, xf: &Xf) -> Option<egui::Rect> {
|
||||
let st = app.session.active()?;
|
||||
let b = st.doc.bounds_of(&st.selection.objects, false)?;
|
||||
Some(egui::Rect::from_points(&xf.quad(b)))
|
||||
}
|
||||
|
||||
/// Where the popover opened from a stop's chip at `chip` goes: beside the selected `art` on the
|
||||
/// first side of it with room for a popover of `size` in `room` (right, left, below, above), so
|
||||
/// the art it recolours stays in view (#862); else beside the chip.
|
||||
fn beside_art(chip: egui::Pos2, art: egui::Rect, size: egui::Vec2, room: egui::Rect) -> egui::Pos2 {
|
||||
const GAP: f32 = 12.0;
|
||||
let x = (chip.x - size.x / 2.0).clamp(room.left(), (room.right() - size.x).max(room.left()));
|
||||
let y = (chip.y - size.y / 2.0).clamp(room.top(), (room.bottom() - size.y).max(room.top()));
|
||||
[
|
||||
egui::pos2(art.right() + GAP, y),
|
||||
egui::pos2(art.left() - GAP - size.x, y),
|
||||
egui::pos2(x, art.bottom() + GAP),
|
||||
egui::pos2(x, art.top() - GAP - size.y),
|
||||
]
|
||||
.into_iter()
|
||||
.find(|p| room.contains_rect(egui::Rect::from_min_size(*p, size)))
|
||||
.unwrap_or(chip + vec2(14.0, 14.0))
|
||||
}
|
||||
|
||||
/// Document swatches as tiles: a click gives the stop (or point) that colour.
|
||||
fn swatch_grid(app: &mut VectorcraftApp, ui: &mut egui::Ui, target: &Target) {
|
||||
let colors = swatch_colors(app);
|
||||
@@ -248,6 +283,28 @@ mod tests {
|
||||
app
|
||||
}
|
||||
|
||||
/// #862: the popover goes beside the art, on the first side with room, not over it.
|
||||
#[test]
|
||||
fn the_popover_goes_beside_the_art_it_edits() {
|
||||
let room = egui::Rect::from_min_size(egui::pos2(0.0, 0.0), egui::vec2(1200.0, 800.0));
|
||||
let size = egui::vec2(272.0, 360.0);
|
||||
let place = |art: egui::Rect| egui::Rect::from_min_size(beside_art(art.center(), art, size, room), size);
|
||||
// Room on the right.
|
||||
let art = egui::Rect::from_min_max(egui::pos2(300.0, 300.0), egui::pos2(500.0, 450.0));
|
||||
assert!(place(art).left() > art.right() && room.contains_rect(place(art)));
|
||||
// Against the right edge: on the left.
|
||||
let art = egui::Rect::from_min_max(egui::pos2(900.0, 300.0), egui::pos2(1150.0, 450.0));
|
||||
assert!(place(art).right() < art.left());
|
||||
// As wide as the room: below it, or above it near the bottom.
|
||||
let art = egui::Rect::from_min_max(egui::pos2(100.0, 20.0), egui::pos2(1100.0, 300.0));
|
||||
assert!(place(art).top() > art.bottom());
|
||||
let art = egui::Rect::from_min_max(egui::pos2(100.0, 420.0), egui::pos2(1100.0, 780.0));
|
||||
assert!(place(art).bottom() < art.top());
|
||||
// Nowhere to go: beside the chip, as before.
|
||||
let art = room.shrink(10.0);
|
||||
assert_eq!(beside_art(art.center(), art, size, room), art.center() + egui::vec2(14.0, 14.0));
|
||||
}
|
||||
|
||||
fn stops(app: &VectorcraftApp) -> Vec<GradientStop> {
|
||||
match selected(app) {
|
||||
Some(Target::Stop(stops, _)) => stops,
|
||||
|
||||
@@ -1497,6 +1497,8 @@
|
||||
Monospaced Monoespaciada
|
||||
More Más
|
||||
More Pathfinder options Más opciones de Buscatrazos
|
||||
More Align options Más opciones de alineación
|
||||
More Paragraph options Más opciones de párrafo
|
||||
More Settings Más ajustes
|
||||
Move Mover
|
||||
Move All Objects Mover todos los objetos
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1496,6 +1496,8 @@
|
||||
Monospaced À chasse fixe
|
||||
More Plus
|
||||
More Pathfinder options Plus d’options Pathfinder
|
||||
More Align options Plus d’options d’alignement
|
||||
More Paragraph options Plus d’options de paragraphe
|
||||
More Settings Plus de réglages
|
||||
Move Déplacer
|
||||
Move All Objects Déplacer tous les objets
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1497,6 +1497,8 @@
|
||||
Monospaced Monospaziato
|
||||
More Altro
|
||||
More Pathfinder options Altre opzioni di Elaborazione tracciati
|
||||
More Align options Altre opzioni di allineamento
|
||||
More Paragraph options Altre opzioni di paragrafo
|
||||
More Settings Altre impostazioni
|
||||
Move Sposta
|
||||
Move All Objects Sposta tutti gli oggetti
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1639,6 +1639,8 @@
|
||||
Monochrome images モノクロイメージ
|
||||
More 多く
|
||||
More Pathfinder options その他のパスファインダーオプション
|
||||
More Align options その他の整列オプション
|
||||
More Paragraph options その他の段落オプション
|
||||
More Settings その他の設定
|
||||
Move 移動
|
||||
Move Anchors アンカーポイントを移動
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1313,6 +1313,8 @@
|
||||
Monochrome images Imagens monocromáticas
|
||||
More Mais
|
||||
More Pathfinder options Mais opções de Localizador de Caminhos
|
||||
More Align options Mais opções de alinhamento
|
||||
More Paragraph options Mais opções de parágrafo
|
||||
More Settings Mais Configurações
|
||||
Move Mover
|
||||
Move Anchors Mover Âncoras
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -1495,6 +1495,8 @@
|
||||
Monospaced Моноширинный
|
||||
More Больше
|
||||
More Pathfinder options Дополнительные параметры обработки контуров
|
||||
More Align options Дополнительные параметры выравнивания
|
||||
More Paragraph options Дополнительные параметры абзаца
|
||||
More Settings Дополнительные настройки
|
||||
Move Переместить
|
||||
Move All Objects Переместить все объекты
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1491,6 +1491,8 @@
|
||||
Monospaced Моноширинний
|
||||
More Більше
|
||||
More Pathfinder options Додаткові параметри обробки контурів
|
||||
More Align options Додаткові параметри вирівнювання
|
||||
More Paragraph options Додаткові параметри абзацу
|
||||
More Settings Додаткові налаштування
|
||||
Move Перемістити
|
||||
Move All Objects Перемістити всі об’єкти
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 632 and column 40.
|
@@ -1455,6 +1455,8 @@
|
||||
Monochrome images 单色图像
|
||||
More 较多
|
||||
More Pathfinder options 更多路径查找器选项
|
||||
More Align options 更多对齐选项
|
||||
More Paragraph options 更多段落选项
|
||||
More Settings 更多设置
|
||||
Move 移动
|
||||
Move All Objects 移动所有对象
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1497,6 +1497,8 @@
|
||||
Monospaced 等寬
|
||||
More 更多
|
||||
More Pathfinder options 更多路徑管理員選項
|
||||
More Align options 更多對齊選項
|
||||
More Paragraph options 更多段落選項
|
||||
More Settings 更多設定
|
||||
Move 移動
|
||||
Move All Objects 移動所有物件
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -52,6 +52,26 @@ pub const DISTRIBUTE: [(&str, &str, &str, &str); 6] = [
|
||||
("dc-dist-right", "Horizontal Distribute Right", "horizontal", "right"),
|
||||
];
|
||||
|
||||
/// The six align buttons of the Control bar and the Properties panel, `size` points square.
|
||||
/// They align to what the Align panel's Align To says (the key object while there is one), as
|
||||
/// its own buttons do (#789).
|
||||
pub fn align_buttons(app: &mut VectorcraftApp, ui: &mut Ui, size: f32) {
|
||||
let n = selection_len(app);
|
||||
let to = align_to(app, ui.ctx());
|
||||
for (icon, tip, axis, v) in [
|
||||
("align-start-vertical", "Horizontal Align Left", "horizontal", "left"),
|
||||
("align-center-vertical", "Horizontal Align Center", "horizontal", "center"),
|
||||
("align-end-vertical", "Horizontal Align Right", "horizontal", "right"),
|
||||
("align-start-horizontal", "Vertical Align Top", "vertical", "top"),
|
||||
("align-center-horizontal", "Vertical Align Center", "vertical", "center"),
|
||||
("align-end-horizontal", "Vertical Align Bottom", "vertical", "bottom"),
|
||||
] {
|
||||
if widgets::icon_button(ui, icon, tl!(tip), false, size).clicked() {
|
||||
app.run("object.align", align_params(json!({axis: v}), to, n)).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the selection has a key object.
|
||||
fn has_key(app: &VectorcraftApp) -> bool {
|
||||
app.session.active().is_some_and(|st| st.selection.key.is_some())
|
||||
@@ -183,4 +203,35 @@ mod tests {
|
||||
app.run("select.key", json!({})).unwrap();
|
||||
assert_eq!(align_to(&app, &ctx), AlignTo::Selection, "no key: the choice again");
|
||||
}
|
||||
|
||||
/// #789: the Control bar's and the Properties panel's align buttons follow Align To too.
|
||||
#[test]
|
||||
fn the_other_align_buttons_align_to_the_align_to_choice() {
|
||||
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 200, "height": 200})).unwrap();
|
||||
let a = app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 10, "height": 10})).unwrap()["id"].clone();
|
||||
let b = app.run("shape.rectangle", json!({"x": 80, "y": 50, "width": 30, "height": 30})).unwrap()["id"].clone();
|
||||
app.run("select.set", json!({"ids": [a, b]})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
set_pstate(&ctx, "align-to", AlignTo::Artboard);
|
||||
// One frame finds the first button (Horizontal Align Right is third); the next clicks it.
|
||||
let at = std::cell::Cell::new(egui::Pos2::ZERO);
|
||||
let frame = |app: &mut VectorcraftApp, events: Vec<egui::Event>| {
|
||||
let mut out = ctx.run_ui(egui::RawInput { events, ..Default::default() }, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
at.set(ui.cursor().min);
|
||||
align_buttons(app, ui, 24.0);
|
||||
});
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
};
|
||||
frame(&mut app, vec![]);
|
||||
let right = at.get() + egui::vec2(2.0 * (24.0 + ctx.global_style().spacing.item_spacing.x) + 12.0, 12.0);
|
||||
let press = |pressed| egui::Event::PointerButton { pos: right, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
|
||||
frame(&mut app, vec![egui::Event::PointerMoved(right), press(true)]);
|
||||
frame(&mut app, vec![press(false)]);
|
||||
let st = app.session.active().unwrap();
|
||||
let x1 = |id: &serde_json::Value| st.doc.bounds_of(&[vectorcraft_doc::NodeId(id.as_u64().unwrap())], false).unwrap().x1;
|
||||
assert_eq!((x1(&a), x1(&b)), (200.0, 200.0), "both to the artboard's right edge");
|
||||
}
|
||||
}
|
||||
@@ -496,7 +496,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
(Some(c), _) => components(mode, &c),
|
||||
(None, _) => vec![0.0; mode.labels().len()],
|
||||
};
|
||||
super::recent_colors_row(app, ui);
|
||||
let recent = super::recent_colors_row(app, ui);
|
||||
widgets::divider(ui);
|
||||
// An edit of the active colour with its displayed components (`new`), or a colour clicked with
|
||||
// Alt held for the inactive proxy (`behind`).
|
||||
@@ -585,6 +585,10 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
*new = Some((c, phase, components(mode, &c)));
|
||||
}
|
||||
};
|
||||
// A recent colour, like the chips, recolours the selected gradient stop (or point).
|
||||
if let Some(c) = recent {
|
||||
pick(c, Live::Released, &mut new);
|
||||
}
|
||||
// None / Black / White chips and the hex field.
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
@@ -691,12 +695,23 @@ mod tests {
|
||||
/// Run the panel for one frame with `events` and `modifiers`; returns the spectrum's rect (when
|
||||
/// the options are shown) and the panel's height.
|
||||
fn frame(app: &mut VectorcraftApp, ctx: &egui::Context, events: Vec<egui::Event>, modifiers: egui::Modifiers) -> (Option<Rect>, f32) {
|
||||
frame_with(app, ctx, events, modifiers, "color-spectrum")
|
||||
}
|
||||
|
||||
/// [`frame`] returning the rect of the panel's widget `id` instead of the spectrum's.
|
||||
fn frame_with(
|
||||
app: &mut VectorcraftApp,
|
||||
ctx: &egui::Context,
|
||||
events: Vec<egui::Event>,
|
||||
modifiers: egui::Modifiers,
|
||||
id: impl std::hash::Hash + std::fmt::Debug,
|
||||
) -> (Option<Rect>, f32) {
|
||||
let mut out = (None, 0.0);
|
||||
let events = std::iter::once(egui::Event::ModifiersChanged(modifiers)).chain(events).collect();
|
||||
let input = egui::RawInput { events, ..Default::default() };
|
||||
let mut full = ctx.run_ui(input, |ui| {
|
||||
show(app, ui);
|
||||
out = (ctx.read_response(ui.id().with("color-spectrum")).map(|r| r.rect), ui.min_rect().height());
|
||||
out = (ctx.read_response(ui.id().with(&id)).map(|r| r.rect), ui.min_rect().height());
|
||||
});
|
||||
full.textures_delta.clear();
|
||||
out
|
||||
@@ -712,6 +727,27 @@ mod tests {
|
||||
frame(app, ctx, vec![], egui::Modifiers::NONE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_recent_colour_recolours_the_selected_gradient_stop() {
|
||||
// #835: it replaced the whole gradient with the colour.
|
||||
let mut app = app();
|
||||
app.run("paint.setFill", json!({"color": "#ff0000"})).unwrap();
|
||||
let stops = json!([{"offset": 0, "color": "#ffffff"}, {"offset": 1, "color": "#000000"}]);
|
||||
app.run("paint.setFill", json!({"gradient": {"stops": stops, "start": [10, 35], "end": [60, 35]}})).unwrap();
|
||||
app.select_tool("gradient");
|
||||
app.run("gradient.selectStop", json!({"index": 1})).unwrap();
|
||||
let red = app.session.recent_colors.iter().position(|c| c.to_hex() == "#ff0000").expect("red is a recent colour");
|
||||
let ctx = egui::Context::default();
|
||||
let none = egui::Modifiers::NONE;
|
||||
let chip = frame_with(&mut app, &ctx, vec![], none, ("recent", red)).0.expect("the chip is drawn").center();
|
||||
let button = |pressed| egui::Event::PointerButton { pos: chip, button: egui::PointerButton::Primary, pressed, modifiers: none };
|
||||
frame(&mut app, &ctx, vec![egui::Event::PointerMoved(chip), button(true)], none);
|
||||
frame(&mut app, &ctx, vec![button(false)], none);
|
||||
let Paint::Gradient(g) = crate::panels::current_paints(&app).0 else { panic!("still a gradient") };
|
||||
let hex: Vec<String> = g.gradient.stops.iter().map(|s| s.color.to_hex()).collect();
|
||||
assert_eq!(hex, ["#ffffff", "#ff0000"]);
|
||||
}
|
||||
|
||||
fn paints_hex(app: &VectorcraftApp) -> (String, String) {
|
||||
let (f, s) = crate::panels::current_paints(app);
|
||||
(f.color().map(|c| c.to_hex()).unwrap_or_default(), s.color().map(|c| c.to_hex()).unwrap_or_default())
|
||||
|
||||
@@ -412,8 +412,8 @@ pub(crate) fn label_or_name(s: &str, built_in: bool) -> &str {
|
||||
}
|
||||
|
||||
/// "Recent Colors" header + a row of chips (the Session's recent colours, which every paint
|
||||
/// command feeds); clicking one applies it to the active proxy (Alt: the inactive one).
|
||||
pub(crate) fn recent_colors_row(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
/// command feeds); returns the one clicked, for the caller to apply.
|
||||
pub(crate) fn recent_colors_row(app: &VectorcraftApp, ui: &mut Ui) -> Option<Color> {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
crate::widgets::subheader(ui, "Recent Colors");
|
||||
let (r, _) = ui.allocate_exact_size(vec2(ui.available_width(), 22.0), Sense::hover());
|
||||
@@ -430,9 +430,7 @@ pub(crate) fn recent_colors_row(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
chosen = Some(*c);
|
||||
}
|
||||
}
|
||||
if let Some(c) = chosen {
|
||||
apply_click(app, ui, json!({"color": color_json(&c)}));
|
||||
}
|
||||
chosen
|
||||
}
|
||||
|
||||
/// A colour as command JSON, keeping its model.
|
||||
|
||||
@@ -60,6 +60,24 @@ fn shown_alignment(justify: Justify, rtl: bool) -> Justify {
|
||||
}
|
||||
}
|
||||
|
||||
/// The seven alignment buttons, `size` points square, the one of `para`'s alignment on (the
|
||||
/// Paragraph panel's and the Properties panel's).
|
||||
pub fn alignment_buttons(app: &mut VectorcraftApp, ui: &mut Ui, para: &ParaStyle, size: f32) {
|
||||
let shown = shown_alignment(para.justify, resolved_rtl(app, para));
|
||||
for (j, icon, tip, id) in ALIGNMENTS {
|
||||
if widgets::icon_button(ui, icon, tip, shown == j, size).clicked() {
|
||||
para_cmd(app, "text.setStyle", json!({"justify": id}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The Hyphenate checkbox of `para`'s paragraphs.
|
||||
pub fn hyphenate_check(app: &mut VectorcraftApp, ui: &mut Ui, para: &ParaStyle) {
|
||||
if widgets::check(ui, tl!("Hyphenate"), para.hyphenate, true) {
|
||||
format(app, json!({"hyphenate": !para.hyphenate}));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let Some((_, para)) = text_style(app) else {
|
||||
@@ -67,14 +85,9 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
return;
|
||||
};
|
||||
let rtl = resolved_rtl(app, ¶);
|
||||
let shown = shown_alignment(para.justify, rtl);
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 3.0;
|
||||
for (j, icon, tip, id) in ALIGNMENTS {
|
||||
if widgets::icon_button(ui, icon, tip, shown == j, 28.0).clicked() {
|
||||
para_cmd(app, "text.setStyle", json!({"justify": id}));
|
||||
}
|
||||
}
|
||||
alignment_buttons(app, ui, ¶, 28.0);
|
||||
// Paragraph direction, with the Middle Eastern (Indic) options.
|
||||
if app.session.prefs.show_indic_options {
|
||||
ui.add_space(6.0);
|
||||
@@ -127,9 +140,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
if pstate::<bool>(ui.ctx(), "pa-hide-options") {
|
||||
return;
|
||||
}
|
||||
if widgets::check(ui, tl!("Hyphenate"), para.hyphenate, true) {
|
||||
format(app, json!({"hyphenate": !para.hyphenate}));
|
||||
}
|
||||
hyphenate_check(app, ui, ¶);
|
||||
// Japanese composition: how punctuation is spaced (JLREQ 3.1), with the East Asian options.
|
||||
if app.session.prefs.show_east_asian_options {
|
||||
ui.horizontal(|ui| {
|
||||
|
||||
@@ -57,21 +57,10 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
divider(ui);
|
||||
section_header(ui, tl!("Align"));
|
||||
ui.horizontal(|ui| {
|
||||
for (icon, tip, p) in [
|
||||
("align-start-vertical", tl!("Horizontal Align Left"), json!({"horizontal": "left"})),
|
||||
("align-center-vertical", tl!("Horizontal Align Center"), json!({"horizontal": "center"})),
|
||||
("align-end-vertical", tl!("Horizontal Align Right"), json!({"horizontal": "right"})),
|
||||
("align-start-horizontal", tl!("Vertical Align Top"), json!({"vertical": "top"})),
|
||||
("align-center-horizontal", tl!("Vertical Align Center"), json!({"vertical": "center"})),
|
||||
("align-end-horizontal", tl!("Vertical Align Bottom"), json!({"vertical": "bottom"})),
|
||||
] {
|
||||
if widgets::icon_button(ui, icon, tip, false, 26.0).clicked() {
|
||||
let mut p = p;
|
||||
if n_sel == 1 {
|
||||
p["to"] = json!("artboard");
|
||||
}
|
||||
app.run("object.align", p).ok();
|
||||
}
|
||||
super::align::align_buttons(app, ui, 26.0);
|
||||
// The rest (Distribute Objects, Distribute Spacing, Align To) is the Align panel's.
|
||||
if widgets::icon_button(ui, "ellipsis", tl!("More Align options"), false, 26.0).clicked() {
|
||||
app.ui.open_panel = Some("align".into());
|
||||
}
|
||||
});
|
||||
if n_sel > 1 {
|
||||
@@ -534,13 +523,17 @@ pub fn type_sections(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
super::character::font_pickers(app, ui, &s, ("font", "font-style"), (w - 4.0, w - 4.0));
|
||||
super::character::metrics_grid(app, ui, "props-char", &s, w, false);
|
||||
section_header(ui, tl!("Paragraph"));
|
||||
ui.horizontal(|ui| {
|
||||
for (icon, j) in [("align-start-vertical", "left"), ("align-center-vertical", "center"), ("align-end-vertical", "right")] {
|
||||
if widgets::icon_button(ui, icon, j, false, 24.0).clicked() {
|
||||
app.run("text.setStyle", json!({"justify": j})).ok();
|
||||
// As in the Paragraph panel: the seven alignments and Hyphenate (#775); the rest is there.
|
||||
if let Some((_, para)) = super::character::text_style(app) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 3.0;
|
||||
super::paragraph::alignment_buttons(app, ui, ¶, 24.0);
|
||||
if widgets::icon_button(ui, "ellipsis", tl!("More Paragraph options"), false, 24.0).clicked() {
|
||||
app.ui.open_panel = Some("paragraph".into());
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
super::paragraph::hyphenate_check(app, ui, ¶);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -603,6 +596,20 @@ mod tests {
|
||||
}
|
||||
|
||||
/// #530: Edit Artboards shows the active artboard, with New Artboard, Delete Artboard and Exit.
|
||||
/// #775: type's Paragraph section has Hyphenate, and its checkbox turns it on.
|
||||
#[test]
|
||||
fn type_paragraph_section_hyphenates() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 200, "height": 100})).unwrap();
|
||||
app.run("text.create", json!({"x": 10, "y": 50, "text": "Paragraph"})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
let texts = frame(&mut app, &ctx, vec![]);
|
||||
click(&mut app, &ctx, &texts, "Hyphenate");
|
||||
let hyphenate = |app: &VectorcraftApp| crate::panels::character::text_style(app).unwrap().1.hyphenate;
|
||||
assert!(hyphenate(&app));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_artboards_adds_artboards_and_exits() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
|
||||
@@ -642,7 +642,10 @@ fn body(app: &mut VectorcraftApp, ui: &mut Ui, salt: &'static str) {
|
||||
}
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
super::recent_colors_row(app, ui);
|
||||
// A recent colour paints the active proxy (Alt: the inactive one), as a swatch does.
|
||||
if let Some(c) = super::recent_colors_row(app, ui) {
|
||||
super::apply_click(app, ui, json!({"color": super::color_json(&c)}));
|
||||
}
|
||||
widgets::divider(ui);
|
||||
widgets::subheader(ui, tl!("Swatch Tiles"));
|
||||
let query = if pstate(ui.ctx(), "swatch-show-find") { widgets::search_field(ui, find_id(), tl!("Find")) } else { String::new() };
|
||||
|
||||
+2
-1
@@ -45,7 +45,8 @@ 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; with Automatic, the GPU that drives a display, and among those the one that drives the primary display; 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.
|
||||
- **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; with Automatic, the GPU that drives a display, and among those the one that drives the primary display; then the power preference; then 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.
|
||||
- **Windows backends:** Direct3D 12, falling back to OpenGL. Vulkan isn't started at all unless `WGPU_BACKEND` asks for it (`WGPU_BACKEND=vulkan`): creating a Vulkan instance loads every installed Vulkan driver into the process, and a faulty one (Intel's `igvk64.dll`, #806) crashed the app before its window appeared.
|
||||
- **The GPU that drives the display.** A GPU without a monitor is the wrong one even when it says it can show the window: on a desktop with a Ryzen's integrated GPU (no monitor) and an NVIDIA card driving both monitors, power saving picked the integrated GPU, Windows had to present every frame across adapters, and the integrated GPU's driver reset under that: the graphics device was lost, or the desktop's compositor went down and every monitor stayed black for minutes (pdfcraft#378). So Automatic asks the system which GPU drives which display and puts that GPU first; the power preference only orders the rest, and decides alone where the system can't say.
|
||||
- **Windows:** `EnumDisplayDevices` (the display devices attached to the desktop, and the primary one), through the `winsafe` crate; the PCI ids in each device id are matched against wgpu's adapters. The registry doesn't tell a user which adapter drives a display, and WMI reports a resolution for a GPU without a monitor, so neither is used.
|
||||
- **Linux:** the connected connectors in `/sys/class/drm` and their cards' PCI ids; a built-in panel (`eDP`, `LVDS`, `DSI`) counts as the primary display.
|
||||
|
||||
Reference in new issue
Block a user