mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-31 15:48:33 +00:00
Merge PR #2711
This commit is contained in:
commit
0da7fd045d
2 files changed
+169
-10
No files matched your search
@@ -514,9 +514,12 @@ impl EffectcraftApp {
|
||||
self.ui.status = error;
|
||||
}
|
||||
}
|
||||
if let Some((panel, pos)) = menu {
|
||||
ctx.data_mut(|d| d.insert_temp(egui::Id::unique("panel-menu"), (panel, pos)));
|
||||
if let Some(open) = menu {
|
||||
ctx.data_mut(|d| d.insert_temp::<panels::OpenPanelMenu>(egui::Id::unique("panel-menu"), open));
|
||||
// the owner window draws it on its next pass
|
||||
ctx.request_repaint();
|
||||
}
|
||||
drop_menu_of_closed_window(&ctx, &self.native_viewports);
|
||||
let hits = DockArea::<PanelKind>::hit_rects(&ctx, area_id());
|
||||
self.register_dock_hits(&hits);
|
||||
panels::panel_menu_popup(self, ui);
|
||||
@@ -651,8 +654,8 @@ struct EffectContent<'a> {
|
||||
doc_actions: Vec<DocAction>,
|
||||
/// Viewer locks clicked.
|
||||
locks: Vec<PanelKind>,
|
||||
/// The ≡ button clicked: its panel and where its menu opens.
|
||||
menu: Option<(PanelKind, egui::Pos2)>,
|
||||
/// The ≡ button clicked: its panel, where its menu opens and the viewport it was clicked in.
|
||||
menu: Option<panels::OpenPanelMenu>,
|
||||
/// Where each panel body was drawn, in drawing order (floating windows last).
|
||||
bodies: Vec<(PanelKind, Rect)>,
|
||||
/// Where each native window's IME descriptor is set up and published.
|
||||
@@ -672,6 +675,8 @@ pub(crate) struct NativeIme {
|
||||
expected: Vec<(egui::ViewportId, usize)>,
|
||||
/// The bodies each native window has drawn so far, in the order the windows began.
|
||||
drawn: Vec<(egui::ViewportId, usize)>,
|
||||
/// Windows that drew their panel menu this pass.
|
||||
menu_drawn: Vec<egui::ViewportId>,
|
||||
/// Windows that published this pass.
|
||||
published: Vec<egui::ViewportId>,
|
||||
}
|
||||
@@ -688,7 +693,7 @@ impl NativeIme {
|
||||
Some((viewport, count))
|
||||
})
|
||||
.collect();
|
||||
Self { expected, drawn: Vec::new(), published: Vec::new() }
|
||||
Self { expected, drawn: Vec::new(), menu_drawn: Vec::new(), published: Vec::new() }
|
||||
}
|
||||
|
||||
/// Count a body of `viewport` that starts drawing. True for the window's first body: its
|
||||
@@ -719,6 +724,18 @@ impl NativeIme {
|
||||
publishes
|
||||
}
|
||||
|
||||
/// Whether the body of `viewport` that just drew is the one that draws the window's panel
|
||||
/// menu: after the window's last body, once per pass.
|
||||
pub(crate) fn menu_turn(&mut self, viewport: egui::ViewportId) -> bool {
|
||||
let drawn = self.drawn.iter().find(|(v, _)| *v == viewport).map_or(0, |(_, n)| *n);
|
||||
let expected = self.expected.iter().find(|(v, _)| *v == viewport).map_or(0, |(_, n)| *n);
|
||||
let turn = drawn >= expected && !self.menu_drawn.contains(&viewport);
|
||||
if turn {
|
||||
self.menu_drawn.push(viewport);
|
||||
}
|
||||
turn
|
||||
}
|
||||
|
||||
/// The native windows whose pass began.
|
||||
pub(crate) fn begun(&self) -> Vec<egui::ViewportId> {
|
||||
self.drawn.iter().map(|(v, _)| *v).collect()
|
||||
@@ -730,6 +747,17 @@ impl NativeIme {
|
||||
}
|
||||
}
|
||||
|
||||
/// A panel menu left open by a panel window that has since closed has nowhere to draw: drop it.
|
||||
/// `windows` are the panel windows that drew this pass.
|
||||
pub(crate) fn drop_menu_of_closed_window(ctx: &egui::Context, windows: &[egui::ViewportId]) {
|
||||
let id = egui::Id::unique("panel-menu");
|
||||
ctx.data_mut(|d| {
|
||||
if d.get_temp::<panels::OpenPanelMenu>(id).is_some_and(|(_, _, owner)| owner != egui::ViewportId::ROOT && !windows.contains(&owner)) {
|
||||
d.remove::<panels::OpenPanelMenu>(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// The OS window craft-ui opens for the floating window `id`.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn window_viewport(id: u64) -> Option<egui::ViewportId> {
|
||||
@@ -790,6 +818,11 @@ impl DockContent<PanelKind> for EffectContent<'_> {
|
||||
if native && self.ime.publishes(viewport, panels::viewer_text::ime_recorded(ui.ctx())) {
|
||||
panels::viewer_text::finish_ime(self.app, ui);
|
||||
}
|
||||
// The ≡ menu of a panel in its own OS window is drawn by that window's pass, after its
|
||||
// last body, at that window's coordinates.
|
||||
if native && self.ime.menu_turn(viewport) {
|
||||
panels::panel_menu_area(self.app, ui);
|
||||
}
|
||||
// The body fills its frame; allocating it lets a floated panel keep its full size.
|
||||
ui.allocate_rect(rect, Sense::hover());
|
||||
// Painted after the body so child backgrounds cannot hide the focus outline.
|
||||
@@ -843,7 +876,7 @@ impl DockContent<PanelKind> for EffectContent<'_> {
|
||||
crate::widgets::focus_outline(ui, &response, &t);
|
||||
if response.clicked() {
|
||||
self.app.ui.focused = *active;
|
||||
self.menu = Some((*active, rect.left_bottom()));
|
||||
self.menu = Some((*active, rect.left_bottom(), ui.ctx().viewport_id()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -300,14 +300,35 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: PanelKind, rect: Rec
|
||||
}
|
||||
}
|
||||
|
||||
/// The ≡ panel menu (opened from a tab).
|
||||
/// The open ≡ panel menu: the panel, where it opens and the viewport that owns the click.
|
||||
pub(crate) type OpenPanelMenu = (PanelKind, egui::Pos2, egui::ViewportId);
|
||||
|
||||
/// The main window's pass of the ≡ panel menu (opened from a tab).
|
||||
pub fn panel_menu_popup(app: &mut EffectcraftApp, ui: &mut egui::Ui) {
|
||||
panel_menu_area(app, ui);
|
||||
}
|
||||
|
||||
/// The ≡ panel menu, drawn in the viewport that owns the click that opened it: a panel in its own
|
||||
/// OS window opens its menu in that window, at that window's coordinates, instead of in the main
|
||||
/// window.
|
||||
pub(crate) fn panel_menu_area(app: &mut EffectcraftApp, ui: &mut egui::Ui) {
|
||||
let id = egui::Id::unique("panel-menu");
|
||||
let Some((panel, pos)) = ui.ctx().data(|d| d.get_temp::<(PanelKind, egui::Pos2)>(id)) else { return };
|
||||
let viewport = ui.ctx().viewport_id();
|
||||
let Some((panel, pos, owner)) = ui.ctx().data(|d| d.get_temp::<OpenPanelMenu>(id)) else { return };
|
||||
if owner != viewport {
|
||||
// a press in another window dismisses the menu, as a press elsewhere in its own does
|
||||
if ui.ctx().input(|i| i.pointer.any_pressed()) {
|
||||
ui.ctx().data_mut(|d| d.remove::<OpenPanelMenu>(id));
|
||||
}
|
||||
return;
|
||||
}
|
||||
let mut close = false;
|
||||
let area = egui::Area::new(id.with("area")).order(egui::Order::Foreground).fixed_pos(pos).show(ui.ctx(), |ui| {
|
||||
egui::Frame::popup(ui.style()).show(ui, |ui| {
|
||||
ui.set_min_width(200.0);
|
||||
// a small window (a floated panel can be narrower than the menu) gets the menu at its width
|
||||
let width = (ui.ctx().content_rect().width() - 16.0).max(0.0);
|
||||
ui.set_min_width(200.0_f32.min(width));
|
||||
ui.set_max_width(width);
|
||||
if ui.button(tr("Close Panel")).clicked() {
|
||||
let _ = app.dock_command(ui.ctx(), craft_ui::docking::Action::Close { panel });
|
||||
close = true;
|
||||
@@ -371,7 +392,7 @@ pub fn panel_menu_popup(app: &mut EffectcraftApp, ui: &mut egui::Ui) {
|
||||
});
|
||||
});
|
||||
if close || crate::widgets::pressed_outside(ui.ctx(), &area.response) {
|
||||
ui.ctx().data_mut(|d| d.remove::<(PanelKind, egui::Pos2)>(id));
|
||||
ui.ctx().data_mut(|d| d.remove::<OpenPanelMenu>(id));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -389,3 +410,108 @@ mod preset_drop_tests {
|
||||
assert_eq!(PresetRef::Text("typewriter".into()).command(), ("layer.applyTextPreset", json!({"preset": "typewriter"})));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod panel_menu_viewport_tests {
|
||||
use super::*;
|
||||
use effectcraft_engine::Session;
|
||||
|
||||
fn menu_id() -> egui::Id {
|
||||
egui::Id::unique("panel-menu")
|
||||
}
|
||||
|
||||
fn open(ctx: &egui::Context, owner: egui::ViewportId) {
|
||||
ctx.data_mut(|d| d.insert_temp::<OpenPanelMenu>(menu_id(), (PanelKind::Timeline, egui::pos2(20.0, 20.0), owner)));
|
||||
}
|
||||
|
||||
fn is_open(ctx: &egui::Context) -> bool {
|
||||
ctx.data(|d| d.get_temp::<OpenPanelMenu>(menu_id())).is_some()
|
||||
}
|
||||
|
||||
/// One pass of `viewport` (a window `width` wide) that draws the menu as the app does: the
|
||||
/// main window through `panel_menu_popup`, a panel's own window through `panel_menu_area`.
|
||||
/// Returns where the menu was drawn in that pass.
|
||||
fn pass(app: &mut EffectcraftApp, ctx: &egui::Context, viewport: egui::ViewportId, width: f32, events: Vec<egui::Event>) -> Option<egui::Rect> {
|
||||
let mut viewports = egui::ViewportIdMap::default();
|
||||
viewports.insert(viewport, egui::ViewportInfo::default());
|
||||
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(width, 400.0));
|
||||
let raw = egui::RawInput { viewport_id: viewport, viewports, screen_rect: Some(screen), events, ..Default::default() };
|
||||
let mut drawn = None;
|
||||
let mut out = ctx.run_ui(raw, |ui| {
|
||||
if viewport == egui::ViewportId::ROOT {
|
||||
panel_menu_popup(app, ui);
|
||||
} else {
|
||||
panel_menu_area(app, ui);
|
||||
}
|
||||
drawn = ui.ctx().memory(|m| m.area_rect(menu_id().with("area")));
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
drawn
|
||||
}
|
||||
|
||||
fn window() -> egui::ViewportId {
|
||||
egui::ViewportId::from_hash_of("panel-window")
|
||||
}
|
||||
|
||||
fn press(pos: egui::Pos2) -> Vec<egui::Event> {
|
||||
vec![
|
||||
egui::Event::PointerMoved(pos),
|
||||
egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed: true, modifiers: egui::Modifiers::NONE },
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_floating_panels_menu_is_drawn_in_its_own_window_not_the_main_one() {
|
||||
let mut app = EffectcraftApp::new(Session::default());
|
||||
let ctx = egui::Context::default();
|
||||
open(&ctx, window());
|
||||
assert_eq!(pass(&mut app, &ctx, egui::ViewportId::ROOT, 1000.0, vec![]), None, "the main window does not draw it");
|
||||
assert!(is_open(&ctx), "and does not drop it");
|
||||
let rect = pass(&mut app, &ctx, window(), 1000.0, vec![]).expect("the panel's window draws it");
|
||||
assert!((rect.min.x - 20.0).abs() < 1.0, "it opens at the window-local position: {rect:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_main_window_panels_menu_stays_in_the_main_window() {
|
||||
let mut app = EffectcraftApp::new(Session::default());
|
||||
let ctx = egui::Context::default();
|
||||
open(&ctx, egui::ViewportId::ROOT);
|
||||
assert!(pass(&mut app, &ctx, window(), 1000.0, vec![]).is_none());
|
||||
assert!(pass(&mut app, &ctx, egui::ViewportId::ROOT, 1000.0, vec![]).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_floating_panels_menu_fits_a_window_narrower_than_the_menu() {
|
||||
let mut app = EffectcraftApp::new(Session::default());
|
||||
let ctx = egui::Context::default();
|
||||
open(&ctx, window());
|
||||
let rect = pass(&mut app, &ctx, window(), 160.0, vec![]).unwrap();
|
||||
assert!(rect.max.x <= 160.5, "the menu fits the 160 pt window: {rect:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_press_in_another_window_closes_a_floating_panels_menu() {
|
||||
let mut app = EffectcraftApp::new(Session::default());
|
||||
let ctx = egui::Context::default();
|
||||
open(&ctx, window());
|
||||
pass(&mut app, &ctx, window(), 1000.0, vec![]);
|
||||
assert!(is_open(&ctx));
|
||||
pass(&mut app, &ctx, egui::ViewportId::ROOT, 1000.0, press(egui::pos2(600.0, 300.0)));
|
||||
assert!(!is_open(&ctx), "a click in the main window dismisses it");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_menu_left_in_a_closed_window_is_dropped() {
|
||||
let ctx = egui::Context::default();
|
||||
let other = egui::ViewportId::from_hash_of("other-window");
|
||||
open(&ctx, window());
|
||||
crate::dock_ui::drop_menu_of_closed_window(&ctx, &[other]);
|
||||
assert!(!is_open(&ctx), "its window is gone");
|
||||
open(&ctx, window());
|
||||
crate::dock_ui::drop_menu_of_closed_window(&ctx, &[window()]);
|
||||
assert!(is_open(&ctx), "its window is still there");
|
||||
open(&ctx, egui::ViewportId::ROOT);
|
||||
crate::dock_ui::drop_menu_of_closed_window(&ctx, &[]);
|
||||
assert!(is_open(&ctx), "the main window is always there");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user