Merge PR #1262
This commit is contained in:
commit
d5b8d52e1d
|
|
@ -27,3 +27,8 @@ sync_github_name = true
|
||||||
|
|
||||||
[people.sanyamsingh001]
|
[people.sanyamsingh001]
|
||||||
display_name = "sanyamsingh001"
|
display_name = "sanyamsingh001"
|
||||||
|
|
||||||
|
[people.switch87]
|
||||||
|
real_name = "Gert Pellin"
|
||||||
|
display_name = "Gert Pellin"
|
||||||
|
sync_github_name = false
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,8 @@ mod tests_sync;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests_titlebar;
|
mod tests_titlebar;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
mod tests_toast;
|
||||||
|
#[cfg(test)]
|
||||||
mod tests_unsaved;
|
mod tests_unsaved;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests_wb_selector;
|
mod tests_wb_selector;
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,9 @@ pub fn resizable_side(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The HUD toast at the bottom centre of the canvas.
|
/// The HUD toast at the bottom centre of the canvas.
|
||||||
|
///
|
||||||
|
/// Drawn as widgets rather than a painted galley so the message can be selected and copied: an
|
||||||
|
/// error here often carries a path or a command the user wants to paste somewhere else.
|
||||||
pub fn toast(app: &mut LightcraftApp, ctx: &egui::Context) {
|
pub fn toast(app: &mut LightcraftApp, ctx: &egui::Context) {
|
||||||
let now = ctx.input(|i| i.time);
|
let now = ctx.input(|i| i.time);
|
||||||
let Some((text, until, label)) = app.ui.toast.clone() else { return };
|
let Some((text, until, label)) = app.ui.toast.clone() else { return };
|
||||||
|
|
@ -79,16 +82,25 @@ pub fn toast(app: &mut LightcraftApp, ctx: &egui::Context) {
|
||||||
let Some(canvas) = app.canvas_rect else { return };
|
let Some(canvas) = app.canvas_rect else { return };
|
||||||
let t = Tokens::get(ctx);
|
let t = Tokens::get(ctx);
|
||||||
let fade = ((until - now) / 0.25).clamp(0.0, 1.0) as f32;
|
let fade = ((until - now) / 0.25).clamp(0.0, 1.0) as f32;
|
||||||
let painter = ctx.layer_painter(egui::LayerId::new(egui::Order::Foreground, egui::Id::new("toast")));
|
let (background, foreground) = label.map(crate::theme::label_toast_colors).unwrap_or((egui::Color32::from_black_alpha(200), t.text));
|
||||||
|
let background = background.gamma_multiply(fade);
|
||||||
|
let foreground = foreground.gamma_multiply(fade);
|
||||||
// long messages (a save warning, the web build's notices) wrap within the canvas
|
// long messages (a save warning, the web build's notices) wrap within the canvas
|
||||||
let wrap = (canvas.width() - 80.0).clamp(200.0, 720.0);
|
let wrap = (canvas.width() - 80.0).clamp(200.0, 720.0);
|
||||||
let (background, foreground) = label.map(crate::theme::label_toast_colors).unwrap_or((egui::Color32::from_black_alpha(200), t.text));
|
egui::Area::new(egui::Id::new("toast"))
|
||||||
let foreground = foreground.gamma_multiply(fade);
|
.order(egui::Order::Foreground)
|
||||||
let galley = painter.layout(text, t.font(14.0), foreground, wrap);
|
.movable(false)
|
||||||
let size = vec2(galley.size().x + 40.0, (galley.size().y + 22.0).max(40.0));
|
.pivot(Align2::CENTER_CENTER)
|
||||||
let r = Rect::from_center_size(pos2(canvas.center().x, canvas.bottom() - 60.0), size);
|
.fixed_pos(pos2(canvas.center().x, canvas.bottom() - 60.0))
|
||||||
painter.rect_filled(r, 6.0, background.gamma_multiply(fade));
|
.show(ctx, |ui| {
|
||||||
painter.galley(r.center() - galley.size() / 2.0, galley, foreground);
|
ui.set_max_width(wrap);
|
||||||
|
egui::Frame::NONE.fill(background).corner_radius(egui::CornerRadius::same(6)).inner_margin(egui::Margin::symmetric(20, 11)).show(
|
||||||
|
ui,
|
||||||
|
|ui| {
|
||||||
|
ui.add(egui::Label::new(egui::RichText::new(text).font(t.font(14.0)).color(foreground)).wrap());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
ctx.request_repaint_after(std::time::Duration::from_millis(30));
|
ctx.request_repaint_after(std::time::Duration::from_millis(30));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
//! The toast is drawn as widgets, not as paint. That is what makes its message selectable: a toast
|
||||||
|
//! often carries a path or a command the user wants somewhere else, and painted text cannot be
|
||||||
|
//! picked up by the pointer at all.
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use crate::headless::Headless;
|
||||||
|
use crate::{LightcraftApp, Services};
|
||||||
|
|
||||||
|
const T: Duration = Duration::from_secs(20);
|
||||||
|
|
||||||
|
fn demo() -> Headless {
|
||||||
|
let app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), Services { png: None, ..Default::default() });
|
||||||
|
let mut h = Headless::new(app, [1200.0, 800.0], 1.0);
|
||||||
|
// let the first renders finish: a render still running when the test binary exits can crash in the
|
||||||
|
// GPU driver's teardown (the preview pool doesn't join its workers)
|
||||||
|
h.settle(T);
|
||||||
|
h
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One event per frame: [`LightcraftApp::raw_input_hook`] takes pointer events singly.
|
||||||
|
fn send(h: &mut Headless, e: egui::Event) {
|
||||||
|
h.app.synthetic.push(e);
|
||||||
|
h.step();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_toast(h: &mut Headless, msg: &str) -> egui::Rect {
|
||||||
|
h.app.ui.toast = Some((msg.to_owned(), f64::MAX, None));
|
||||||
|
h.step();
|
||||||
|
h.step();
|
||||||
|
h.view.ctx.memory(|m| m.area_rect(egui::Id::new("toast"))).expect("the toast is laid out as a widget, not paint")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_toast_is_a_widget_the_pointer_reaches() {
|
||||||
|
let mut h = demo();
|
||||||
|
let msg = "Exported frame to /tmp/scene-0001.png";
|
||||||
|
let area = show_toast(&mut h, msg);
|
||||||
|
|
||||||
|
assert!(area.is_positive(), "the toast has a rect of its own: {area:?}");
|
||||||
|
assert!(h.painted_text().iter().any(|t| t.contains("Exported frame")), "the message is still painted");
|
||||||
|
|
||||||
|
// The pointer reaches the message: egui offers the text cursor over it. Painted text is never
|
||||||
|
// hit-tested, so this is the difference that makes selection possible at all.
|
||||||
|
send(&mut h, egui::Event::PointerMoved(area.center()));
|
||||||
|
assert_eq!(h.last_cursor, egui::CursorIcon::Text, "the toast text is a text target");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_toast_still_wraps_a_long_message_inside_the_canvas() {
|
||||||
|
let mut h = demo();
|
||||||
|
let msg = "The SAM 3 download failed: the file is 4.1 GB and the connection dropped at 98 %, so nothing was installed and the masks are still unavailable";
|
||||||
|
let area = show_toast(&mut h, msg);
|
||||||
|
|
||||||
|
let canvas = h.app.canvas_rect.expect("the demo has a canvas");
|
||||||
|
assert!(area.width() <= canvas.width() - 80.0, "the toast wraps within the canvas: {area:?}");
|
||||||
|
assert!(h.painted_text().iter().any(|t| t.contains("SAM 3 download failed")), "the whole message is painted");
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue