use egui::{Pos2, Vec2};
#[derive(Default)]
pub struct ViewportSignals {
pub close_requested: bool,
pub live_pos: Option<Pos2>,
}
impl ViewportSignals {
fn read(ctx: &egui::Context) -> Self {
ctx.input(|i| {
let vp = i.viewport();
Self {
close_requested: vp.close_requested(),
live_pos: vp.outer_rect.map(|r| r.min),
}
})
}
}
pub fn show_detached(
ctx: &egui::Context,
id: egui::Id,
title: &str,
size: Vec2,
position: Option<Pos2>,
mut add_contents: impl FnMut(&mut egui::Ui),
) -> ViewportSignals {
let viewport_id = egui::ViewportId::from_hash_of(id);
let mut builder = egui::ViewportBuilder::default()
.with_title(title)
.with_inner_size(size);
if let Some(pos) = position {
builder = builder.with_position(pos);
}
let mut signals = ViewportSignals::default();
ctx.show_viewport_immediate(viewport_id, builder, |ui, _class| {
add_contents(ui);
signals = ViewportSignals::read(ui.ctx());
});
signals
}
pub struct DetachedWindow {
id: egui::Id,
size: Vec2,
placement: Option<Pos2>,
remembered: Option<Pos2>,
}
impl DetachedWindow {
pub fn new(id: egui::Id, size: Vec2) -> Self {
Self {
id,
size,
placement: None,
remembered: None,
}
}
pub fn id(&self) -> egui::Id {
self.id
}
pub fn size(&self) -> Vec2 {
self.size
}
pub fn position(&mut self, ctx: &egui::Context) -> Option<Pos2> {
if self.placement.is_none() {
self.placement = self
.remembered
.or_else(|| beside_main_window(ctx, self.size));
}
self.placement
}
pub fn apply_signals(&mut self, signals: &ViewportSignals, open: &mut bool) {
if let Some(pos) = signals.live_pos {
self.remembered = Some(pos);
}
if signals.close_requested {
*open = false;
self.placement = None;
}
}
}
pub(crate) fn beside_main_window(ctx: &egui::Context, size: Vec2) -> Option<Pos2> {
let (win, monitor) = ctx.input(|i| {
let vp = i.viewport();
(vp.outer_rect, vp.monitor_size)
});
Some(placement_beside(win?, monitor?, size, 5.0))
}
pub(crate) fn placement_beside(
win: egui::Rect,
monitor_size: Vec2,
size: Vec2,
margin: f32,
) -> Pos2 {
let profile_w = size.x;
let space_left = win.left(); let space_right = monitor_size.x - win.right();
let top = win.top() + (win.height() - size.y) / 2.0;
let left = if profile_w < space_right {
win.right() + margin
} else if profile_w < space_left {
(win.left() - profile_w - margin).max(0.0)
} else if space_left > space_right {
0.0
} else {
monitor_size.x - profile_w
};
egui::pos2(left, top)
}
#[cfg(test)]
mod tests {
use super::placement_beside;
use egui::{Rect, pos2, vec2};
const MARGIN: f32 = 5.0;
fn approx(a: f32, b: f32) {
assert!((a - b).abs() < 1e-4, "expected {b}, got {a}");
}
#[test]
fn places_to_the_right_when_it_fits() {
let win = Rect::from_min_max(pos2(100.0, 0.0), pos2(600.0, 400.0));
let p = placement_beside(win, vec2(1200.0, 800.0), vec2(420.0, 320.0), MARGIN);
approx(p.x, 605.0); approx(p.y, 40.0); }
#[test]
fn places_to_the_left_when_right_is_too_tight() {
let win = Rect::from_min_max(pos2(600.0, 0.0), pos2(1180.0, 400.0));
let p = placement_beside(win, vec2(1200.0, 800.0), vec2(420.0, 320.0), MARGIN);
approx(p.x, 175.0); }
#[test]
fn left_placement_clamps_to_screen_edge() {
let win = Rect::from_min_max(pos2(422.0, 0.0), pos2(900.0, 400.0));
let p = placement_beside(win, vec2(1000.0, 500.0), vec2(420.0, 320.0), MARGIN);
approx(p.x, 0.0); }
#[test]
fn overflow_both_sides_picks_the_roomier_edge() {
let win = Rect::from_min_max(pos2(300.0, 0.0), pos2(900.0, 400.0));
let p = placement_beside(win, vec2(1000.0, 500.0), vec2(420.0, 320.0), MARGIN);
approx(p.x, 0.0);
let win = Rect::from_min_max(pos2(80.0, 0.0), pos2(400.0, 400.0));
let p = placement_beside(win, vec2(1000.0, 500.0), vec2(700.0, 320.0), MARGIN);
approx(p.x, 300.0); }
}