use super::*;
use crate::draw_ctx::DrawCtx;
use crate::event::{Event, EventResult};
use crate::geometry::{Point, Rect};
use crate::widget::active_modal_path;
use crate::widgets::rich_text::{
single_font_resolver, Block, RichCommand, RichDoc, RichEditHandle, RichTextEdit, RichTextToolbar,
};
use crate::widgets::window::{ClickAwayAction, Window};
use crate::{ColorWheelPicker, Stack};
use std::cell::Cell;
use std::rc::Rc;
use std::sync::Arc;
fn unit_scale() {
crate::device_scale::set_device_scale(1.0);
crate::ux_scale::set_ux_scale(1.0);
}
const VP_W: f64 = 420.0;
const VP_H: f64 = 520.0;
fn modal_window_world(app: &App) -> Rect {
let path = active_modal_path(app.root()).expect("a modal dialog must be open");
let mut widget: &dyn Widget = app.root();
let (mut ox, mut oy) = (0.0, 0.0);
for &idx in &path {
let child: &Box<dyn Widget> = &widget.children()[idx];
let b = child.bounds();
ox += b.x;
oy += b.y;
widget = child.as_ref();
}
let b = widget.bounds();
Rect::new(ox, oy, b.width, b.height)
}
fn modal_open(app: &App) -> bool {
active_modal_path(app.root()).is_some()
}
fn click_world(app: &mut App, world: Point) {
let screen_y = VP_H - world.y;
app.on_mouse_down(world.x, screen_y, MouseButton::Left, Modifiers::default());
app.on_mouse_up(world.x, screen_y, MouseButton::Left, Modifiers::default());
}
struct Slot {
bounds: Rect,
slot: Rect,
children: Vec<Box<dyn Widget>>,
}
impl Widget for Slot {
fn type_name(&self) -> &'static str {
"Slot"
}
fn bounds(&self) -> Rect {
self.bounds
}
fn set_bounds(&mut self, b: Rect) {
self.bounds = b;
}
fn children(&self) -> &[Box<dyn Widget>] {
&self.children
}
fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
&mut self.children
}
fn layout(&mut self, available: Size) -> Size {
self.bounds = Rect::new(0.0, 0.0, available.width, available.height);
let s = self.slot;
let child = &mut self.children[0];
child.layout(Size::new(s.width, s.height));
child.set_bounds(s);
available
}
fn paint(&mut self, _ctx: &mut dyn DrawCtx) {}
fn on_event(&mut self, _event: &Event) -> EventResult {
EventResult::Ignored
}
}
fn build_toolbar_app() -> (App, RichEditHandle) {
let font = Arc::new(crate::text::Font::from_slice(TEST_FONT).unwrap());
let doc = RichDoc::from_blocks(vec![Block::plain("colour me")]);
let editor = RichTextEdit::new(doc, single_font_resolver(Arc::clone(&font)));
let handle = editor.handle();
handle.select_all();
let toolbar = RichTextToolbar::new(handle.clone(), Arc::clone(&font));
let slot = Slot {
bounds: Rect::default(),
slot: Rect::new(0.0, 460.0, VP_W, 48.0),
children: vec![Box::new(toolbar)],
};
let root = Stack::new().with_hit_children_only(false).add(Box::new(slot));
(App::new(Box::new(root)), handle)
}
fn open_text_color(app: &mut App) {
{
let slot = &mut app.root_mut().children_mut()[0];
let toolbar = &mut slot.children_mut()[0];
let col = &mut toolbar.children_mut()[0];
let row1 = &mut col.children_mut()[0];
let swatch = &mut row1.children_mut()[5];
let b = swatch.bounds();
let pos = Point::new(b.width * 0.5, b.height * 0.5);
swatch.on_event(&Event::MouseDown {
pos,
button: MouseButton::Left,
modifiers: Modifiers::default(),
});
swatch.on_event(&Event::MouseUp {
pos,
button: MouseButton::Left,
modifiers: Modifiers::default(),
});
}
app.layout(Size::new(VP_W, VP_H));
}
#[test]
fn clickaway_commits_changed_color_and_closes() {
unit_scale();
let (mut app, handle) = build_toolbar_app();
app.layout(Size::new(VP_W, VP_H));
open_text_color(&mut app);
assert!(modal_open(&app), "the picker dialog must be open");
assert!(handle.is_previewing(), "opening begins a preview session");
let blue = Color::rgb(0.0, 0.0, 1.0);
handle.exec(&RichCommand::SetTextColor(blue));
app.layout(Size::new(VP_W, VP_H));
assert!(handle.is_preview_dirty(), "the preview mutated the document");
let w = modal_window_world(&app);
let outside = Point::new(w.x + w.width + 12.0, w.y + w.height * 0.5);
click_world(&mut app, outside);
app.layout(Size::new(VP_W, VP_H));
assert!(!modal_open(&app), "click-away must close the dialog");
assert!(!handle.is_previewing(), "click-away must end the preview session");
assert_eq!(
handle.common_style_of_selection().text_color,
Some(Some(blue)),
"a changed colour must be COMMITTED (kept) on click-away"
);
}
#[test]
fn clickaway_without_change_closes_and_keeps_document() {
unit_scale();
let (mut app, handle) = build_toolbar_app();
app.layout(Size::new(VP_W, VP_H));
let before = handle.common_style_of_selection().text_color;
open_text_color(&mut app);
assert!(modal_open(&app), "the picker dialog must be open");
assert!(handle.is_previewing(), "opening begins a preview session");
assert!(!handle.is_preview_dirty(), "nothing changed yet");
let w = modal_window_world(&app);
let outside = Point::new(w.x + w.width + 12.0, w.y + w.height * 0.5);
click_world(&mut app, outside);
app.layout(Size::new(VP_W, VP_H));
assert!(!modal_open(&app), "click-away must close the dialog");
assert!(!handle.is_previewing(), "click-away must end the preview session");
assert_eq!(
handle.common_style_of_selection().text_color,
before,
"an untouched click-away must leave the document unchanged"
);
}
#[test]
fn clickaway_right_press_also_dismisses() {
unit_scale();
let (mut app, handle) = build_toolbar_app();
app.layout(Size::new(VP_W, VP_H));
open_text_color(&mut app);
assert!(modal_open(&app), "the picker dialog must be open");
let w = modal_window_world(&app);
let outside = Point::new(w.x + w.width + 12.0, w.y + w.height * 0.5);
let screen_y = VP_H - outside.y;
app.on_mouse_down(outside.x, screen_y, MouseButton::Right, Modifiers::default());
app.on_mouse_up(outside.x, screen_y, MouseButton::Right, Modifiers::default());
app.layout(Size::new(VP_W, VP_H));
assert!(!modal_open(&app), "a right-press outside must close the dialog");
assert!(!handle.is_previewing(), "right-press click-away ends the preview");
}
struct ComboUnderlay {
bounds: Rect,
children: Vec<Box<dyn Widget>>,
combo_rect: Rc<Cell<Rect>>,
}
impl Widget for ComboUnderlay {
fn bounds(&self) -> Rect {
self.bounds
}
fn set_bounds(&mut self, b: Rect) {
self.bounds = b;
}
fn children(&self) -> &[Box<dyn Widget>] {
&self.children
}
fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
&mut self.children
}
fn layout(&mut self, available: Size) -> Size {
self.bounds = Rect::new(0.0, 0.0, available.width, available.height);
let r = self.combo_rect.get();
let child = &mut self.children[0];
child.layout(Size::new(r.width, r.height));
child.set_bounds(r);
available
}
fn paint(&mut self, _ctx: &mut dyn DrawCtx) {}
fn on_event(&mut self, _event: &Event) -> EventResult {
EventResult::Ignored
}
}
fn combo_open(app: &App) -> bool {
let combo = &app.root().children()[0].children()[0];
combo
.properties()
.into_iter()
.find(|(k, _)| *k == "open")
.map(|(_, v)| v == "true")
.unwrap_or(false)
}
#[test]
fn clickaway_does_not_activate_widget_underneath() {
unit_scale();
let font = Arc::new(crate::text::Font::from_slice(TEST_FONT).unwrap());
let combo_rect = Rc::new(Cell::new(Rect::new(0.0, 0.0, 100.0, 24.0)));
let combo = ComboBox::new(vec!["Zero", "One", "Two"], 0, Arc::clone(&font));
let underlay = ComboUnderlay {
bounds: Rect::default(),
children: vec![Box::new(combo)],
combo_rect: Rc::clone(&combo_rect),
};
let picker = ColorWheelPicker::new(Color::rgb(0.2, 0.45, 0.88), Arc::clone(&font))
.with_show_alpha(true)
.with_font_size(12.0);
let dialog =
crate::color_wheel_picker_dialog_with_on_close(picker, "Text colour", move |_reason| {});
let root = Stack::new()
.with_hit_children_only(false)
.add(Box::new(underlay))
.add_aligned(dialog);
let mut app = App::new(Box::new(root));
app.layout(Size::new(VP_W, VP_H));
let w = modal_window_world(&app);
let over_combo = Point::new(w.x + w.width + 20.0, w.y + w.height * 0.5);
combo_rect.set(Rect::new(over_combo.x - 50.0, over_combo.y - 12.0, 100.0, 24.0));
app.layout(Size::new(VP_W, VP_H));
assert!(!combo_open(&app), "combo starts closed");
assert!(modal_open(&app), "dialog starts open");
click_world(&mut app, over_combo);
app.layout(Size::new(VP_W, VP_H));
assert!(!modal_open(&app), "click-away must close the dialog");
assert!(
!combo_open(&app),
"the click that closed the dialog must NOT open the combo underneath"
);
}
fn drag(app: &mut App, from: Point, to: Point) {
app.on_mouse_down(from.x, VP_H - from.y, MouseButton::Left, Modifiers::default());
app.on_mouse_move(to.x, VP_H - to.y);
}
fn end_drag(app: &mut App, at: Point) {
app.on_mouse_up(at.x, VP_H - at.y, MouseButton::Left, Modifiers::default());
}
fn paint_once(app: &mut App) {
app.layout(Size::new(VP_W, VP_H));
let mut fb = Framebuffer::new(VP_W as u32, VP_H as u32);
let mut ctx = GfxCtx::new(&mut fb);
ctx.clear(Color::rgba(1.0, 1.0, 1.0, 1.0));
app.paint(&mut ctx);
}
#[test]
fn modal_drag_escapes_slot_and_clamps_to_viewport() {
unit_scale();
let font = Arc::new(crate::text::Font::from_slice(TEST_FONT).unwrap());
let content = SizedBox::new().with_width(120.0).with_height(150.0);
let win = Window::new("Dlg", Arc::clone(&font), Box::new(content))
.with_bounds(Rect::new(20.0, 20.0, 120.0, 150.0))
.with_auto_size(false)
.with_resizable(false)
.with_constrain(true)
.with_gl_backbuffer(false)
.with_modal(true)
.with_click_away(ClickAwayAction::None);
let slot = Slot {
bounds: Rect::default(),
slot: Rect::new(20.0, 20.0, 60.0, 60.0),
children: vec![Box::new(win)],
};
let root = Stack::new().with_hit_children_only(false).add(Box::new(slot));
let mut app = App::new(Box::new(root));
paint_once(&mut app);
let w0 = modal_window_world(&app);
let grab = Point::new(w0.x + w0.width * 0.5, w0.y + w0.height - 8.0);
drag(&mut app, grab, Point::new(300.0, 340.0));
let w1 = modal_window_world(&app);
assert!(
w1.x > 60.0 && w1.y > 60.0,
"the drag must ESCAPE the 60px slot (got {w1:?}) — it is no longer caged"
);
assert!(
w1.x + w1.width <= VP_W + 0.5 && w1.y + w1.height <= VP_H + 0.5,
"the dragged window must stay within the viewport (got {w1:?})"
);
end_drag(&mut app, Point::new(300.0, 340.0));
let w1 = modal_window_world(&app);
let grab = Point::new(w1.x + w1.width * 0.5, w1.y + w1.height - 8.0);
drag(&mut app, grab, Point::new(900.0, 900.0));
end_drag(&mut app, Point::new(900.0, 900.0));
paint_once(&mut app);
let w2 = modal_window_world(&app);
assert!(
(w2.x + w2.width) <= VP_W + 0.5 && (w2.y + w2.height) <= VP_H + 0.5,
"dragging past the viewport must clamp the window fully on-screen (got {w2:?})"
);
}