mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-03 06:52:21 -06:00
Tab in a value being typed (hot text) dropped the keyboard: egui moved the focus to the next widget, and the inline editors asked for the focus again every frame, which also reset what keys a field keeps for itself. As in After Effects, Tab now commits the value and types in the next value field of the same panel (the next dimension, then the next property's), Shift+Tab the previous one. The hot-text fields of each panel register in drawing order; the dock tags each panel's Ui so the order stays within it. `keep_focus` asks for the focus only when the field doesn't have it. Tests: tab_commits_a_value_and_moves_to_the_next (ui_inline_edit: Timeline and Effect Controls). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
464 lines
24 KiB
Rust
464 lines
24 KiB
Rust
//! The dock area: lays out the docked tree (or the maximized panel), draws each group's chrome
|
|
//! and body, floating (undocked) panel groups over it, and tab drag-and-drop: drag a tab onto a
|
|
//! group to tab it there (center) or split it off to a side (After Effects' drop zones, with a
|
|
//! highlight of where it will land); hold Cmd/Ctrl when releasing, or drop outside every group,
|
|
//! to float it. `` ` `` maximizes the panel under the pointer.
|
|
|
|
use egui::{Color32, Rect, Sense, Stroke, StrokeKind, pos2, vec2};
|
|
|
|
use crate::dock::{self, DockAction, DockNode, Group, Layout, PanelKind, Zone};
|
|
use crate::{EffectcraftApp, panels};
|
|
|
|
fn drag_id() -> egui::Id {
|
|
egui::Id::new("dock-tab-drag")
|
|
}
|
|
|
|
impl EffectcraftApp {
|
|
/// Tab labels that name what the panel shows, as After Effects does: "Composition Intro",
|
|
/// "Effect Controls Title", "Properties: Title" (the Timeline's tabs are its comps, see
|
|
/// [`Self::tab_docs`]).
|
|
fn tab_titles(&self) -> Vec<(PanelKind, String)> {
|
|
let mut out = Vec::new();
|
|
// ScriptUI panels are named after their script.
|
|
for w in self.session.script_ui.windows.iter().filter(|w| w.kind == effectcraft_engine::scriptui::WindowKind::Panel) {
|
|
if let Some(t) = panels::scriptui_view::panel_title(self, w.id) {
|
|
out.push((PanelKind::ScriptPanel(w.id), t));
|
|
}
|
|
}
|
|
// Each Composition viewer is named after the comp it shows.
|
|
for id in std::iter::once(0).chain(self.ui.viewers.keys().copied().filter(|v| *v != 0)) {
|
|
if let Some(t) = panels::viewers::title(self, id) {
|
|
out.push((panels::viewers::panel(id), t));
|
|
}
|
|
}
|
|
let Some(comp) = self.session.active_comp() else { return out };
|
|
if let Some(l) = self.session.state.selected_layers.first().and_then(|id| comp.layer(*id)) {
|
|
out.push((PanelKind::EffectControls, format!("Effect Controls {}", l.name)));
|
|
out.push((PanelKind::Properties, format!("Properties: {}", l.name)));
|
|
}
|
|
out
|
|
}
|
|
|
|
/// A comp's label colour as a tab swatch (faint without a label).
|
|
fn comp_swatch(&self, cid: effectcraft_engine::project::ItemId) -> Color32 {
|
|
match self.session.project.item(cid).map(|i| i.label) {
|
|
Some(l) if l != effectcraft_engine::color::Label::None => self.tokens.label(l),
|
|
_ => self.tokens.text_faint,
|
|
}
|
|
}
|
|
|
|
/// The Timeline's tabs: one per open comp, as in After Effects (opening a precomp adds a tab
|
|
/// next to its parent's instead of replacing it).
|
|
fn tab_docs(&self) -> Vec<(PanelKind, Vec<dock::DocTab>)> {
|
|
let st = &self.session.state;
|
|
let locked = self.ui.locked_tabs.contains(&PanelKind::Timeline.id());
|
|
let docs = st
|
|
.open_comps
|
|
.iter()
|
|
.filter_map(|id| {
|
|
let it = self.session.project.item(*id).filter(|i| i.as_comp().is_some())?;
|
|
let deco = dock::TabDeco { swatch: self.comp_swatch(*id), locked, viewer: true };
|
|
Some(dock::DocTab { id: id.0, title: it.name.clone(), deco: Some(deco), active: st.active_comp == Some(*id) })
|
|
})
|
|
.collect();
|
|
vec![(PanelKind::Timeline, docs)]
|
|
}
|
|
|
|
/// The Composition and Timeline tabs' close button, label-colour swatch and viewer lock.
|
|
fn tab_decos(&self) -> Vec<(PanelKind, dock::TabDeco)> {
|
|
let Some(cid) = self.session.active_comp_id() else { return vec![] };
|
|
let swatch = self.comp_swatch(cid);
|
|
let mut v: Vec<(PanelKind, dock::TabDeco)> =
|
|
vec![(PanelKind::Timeline, dock::TabDeco { swatch, locked: self.ui.locked_tabs.contains(&PanelKind::Timeline.id()), viewer: true })];
|
|
// Every Composition viewer: its comp's swatch and its own lock.
|
|
for id in std::iter::once(0).chain(self.ui.viewers.keys().copied().filter(|v| *v != 0)) {
|
|
let p = panels::viewers::panel(id);
|
|
let swatch = panels::viewers::comp_of(self, id).map_or(self.tokens.text_faint, |c| self.comp_swatch(c));
|
|
v.push((p, dock::TabDeco { swatch, locked: self.ui.locked_tabs.contains(&p.id()), viewer: true }));
|
|
}
|
|
// Effect Controls carries the selected layer's label colour.
|
|
if let Some(l) = self.session.active_comp().and_then(|c| self.session.state.selected_layers.first().and_then(|id| c.layer(*id)))
|
|
&& l.label != effectcraft_engine::color::Label::None
|
|
{
|
|
v.push((PanelKind::EffectControls, dock::TabDeco { swatch: self.tokens.label(l.label), locked: false, viewer: false }));
|
|
}
|
|
v
|
|
}
|
|
|
|
/// Settings ▸ Appearance ▸ Use Label Color for Related Tabs: the Composition and Timeline
|
|
/// tabs carry their comp's label colour, Effect Controls and Properties the layer's.
|
|
fn label_tab_marks(&self, ui: &egui::Ui) {
|
|
if !self.session.prefs.appearance.use_label_color_for_tabs {
|
|
return;
|
|
}
|
|
let Some(cid) = self.session.active_comp_id() else { return };
|
|
let comp_label = self.session.project.item(cid).map(|i| i.label);
|
|
let layer_label = self.session.active_comp().and_then(|c| self.session.state.selected_layers.first().and_then(|l| c.layer(*l))).map(|l| l.label);
|
|
// (the Composition, Timeline and Effect Controls tabs carry their swatch already)
|
|
let _ = comp_label;
|
|
for (p, label) in [(PanelKind::Properties, layer_label)] {
|
|
let (Some(label), Some(e)) = (label, self.auto.find(&format!("panel.tab.{}", p.id()))) else { continue };
|
|
if label == effectcraft_engine::color::Label::None {
|
|
continue;
|
|
}
|
|
let r = Rect::from_min_size(pos2(e.rect[0] + 2.0, e.rect[1] + 9.0), vec2(4.0, (e.rect[3] - 16.0).max(6.0)));
|
|
ui.painter().rect_filled(r, 1.0, self.tokens.label(label));
|
|
}
|
|
}
|
|
|
|
/// Edit the full layout (docked tree + floating groups) with a [`Layout`] operation.
|
|
pub fn edit_layout(&mut self, f: impl FnOnce(&mut Layout) -> bool) -> bool {
|
|
let mut l = Layout { root: self.ui.dock.clone(), floating: self.ui.floating.clone() };
|
|
if !f(&mut l) {
|
|
return false;
|
|
}
|
|
self.ui.dock = l.root;
|
|
self.ui.floating = l.floating;
|
|
if self.ui.maximized.is_some_and(|m| !self.ui.dock.contains(m)) {
|
|
self.ui.maximized = None;
|
|
}
|
|
true
|
|
}
|
|
|
|
/// Close a panel wherever it is (docked or floating).
|
|
pub fn close_panel(&mut self, p: PanelKind) {
|
|
// Closing a ScriptUI panel closes its script window (its script's onClose runs).
|
|
if let PanelKind::ScriptPanel(id) = p
|
|
&& self.session.script_ui.window(id).is_some()
|
|
&& let Err(e) = self.session.execute("scriptui.close", serde_json::json!({"window": id}))
|
|
{
|
|
self.ui.status = e.to_string();
|
|
}
|
|
self.ui.dock.close(p);
|
|
self.edit_layout(|l| l.unfloat(p));
|
|
panels::viewers::on_close(self, p);
|
|
if self.ui.maximized == Some(p) {
|
|
self.ui.maximized = None;
|
|
}
|
|
}
|
|
|
|
/// Toggle the maximized panel (`p`, or none).
|
|
pub fn toggle_maximize(&mut self, p: PanelKind) {
|
|
self.ui.maximized = if self.ui.maximized == Some(p) || !self.ui.dock.contains(p) { None } else { Some(p) };
|
|
}
|
|
|
|
/// The docked panel whose group is under `pos` (from the last frame), else the focused one.
|
|
pub fn panel_at(&self, pos: Option<egui::Pos2>) -> PanelKind {
|
|
pos.and_then(|p| self.dock_rects.iter().rev().find(|(_, r)| r.contains(p)).map(|(k, _)| *k)).unwrap_or(self.ui.focused)
|
|
}
|
|
|
|
fn panel_body(&mut self, ui: &mut egui::Ui, p: PanelKind, rect: Rect) {
|
|
self.auto.add(&format!("panel.{}", p.id()), rect, p.title());
|
|
let mut content = rect.shrink(1.0);
|
|
let viewer = p == panels::viewers::active_panel(self);
|
|
if viewer && !self.ui.start_screen && panels::precomp::has_flow(self) {
|
|
// Composition Navigator: the flow of nested comps above the viewer.
|
|
let nav = Rect::from_min_size(content.min, vec2(content.width(), panels::precomp::NAV_H));
|
|
let mut child = ui.new_child(egui::UiBuilder::new().max_rect(nav).id_salt("comp-navigator"));
|
|
child.set_clip_rect(nav.intersect(ui.clip_rect()));
|
|
panels::precomp::navigator(self, &mut child, nav);
|
|
content.min.y = nav.max.y;
|
|
}
|
|
let mut child = ui.new_child(
|
|
egui::UiBuilder::new().max_rect(content).id_salt(("panel", p.id())).ui_stack_info(egui::UiStackInfo::default().with_tag(crate::widgets::PANEL_TAG)),
|
|
);
|
|
child.set_clip_rect(content.intersect(ui.clip_rect()));
|
|
panels::show(self, &mut child, p, content);
|
|
if viewer && !self.ui.start_screen {
|
|
panels::anim_tools::sketch_overlay(self, &mut child);
|
|
}
|
|
}
|
|
|
|
pub(crate) fn dock_area(&mut self, ui: &mut egui::Ui, body: Rect) {
|
|
let t = self.tokens;
|
|
let ctx = ui.ctx().clone();
|
|
if self.ui.start_screen {
|
|
// Home covers the whole workspace, as in After Effects (the panels wait behind it).
|
|
let mut child = ui.new_child(egui::UiBuilder::new().max_rect(body).id_salt("home"));
|
|
child.set_clip_rect(body.intersect(ui.clip_rect()));
|
|
panels::home::show(self, &mut child, body);
|
|
return;
|
|
}
|
|
// The maximized panel fills the area; the tree is kept as it is.
|
|
let maximized = self.ui.maximized.filter(|m| self.ui.dock.contains(*m));
|
|
let mut dock = match maximized {
|
|
Some(m) => DockNode::Tabs { panels: vec![m], active: 0 },
|
|
None => std::mem::replace(&mut self.ui.dock, DockNode::Tabs { panels: vec![], active: 0 }),
|
|
};
|
|
let mut groups = Vec::new();
|
|
dock::layout(ui, &mut dock, body, &t, "", &mut groups, &mut self.auto);
|
|
let mut actions = Vec::new();
|
|
let titles = self.tab_titles();
|
|
let decos = self.tab_decos();
|
|
let docs = self.tab_docs();
|
|
let title = |p: PanelKind| titles.iter().find(|(k, _)| *k == p).map(|(_, s)| s.clone()).unwrap_or_else(|| p.title().to_string());
|
|
let info = dock::TabInfo { title: &title, decos: &decos, docs: &docs };
|
|
// Where each group's tabs are (tab drags show where a tab will land between them).
|
|
let mut tab_rects: Vec<(String, Vec<(PanelKind, Rect)>)> = vec![];
|
|
for g in &groups {
|
|
let c = dock::draw_group_chrome(ui, g, &t, &mut self.auto, &info);
|
|
actions.extend(c.actions);
|
|
tab_rects.push((g.path.clone(), c.tabs));
|
|
}
|
|
self.label_tab_marks(ui);
|
|
if maximized.is_none() {
|
|
self.ui.dock = dock;
|
|
}
|
|
self.dock_rects = groups.iter().filter_map(|g| Some((*g.panels.get(g.active)?, g.rect))).collect();
|
|
for g in &groups {
|
|
let Some(p) = g.panels.get(g.active).copied() else { continue };
|
|
if g.content.height() < 2.0 {
|
|
continue; // a collapsed stacked panel: header only
|
|
}
|
|
self.panel_body(ui, p, g.content);
|
|
}
|
|
for g in &groups {
|
|
if g.panels.get(g.active) == Some(&self.ui.focused) {
|
|
ui.painter().rect_stroke(g.rect, 0.0, Stroke::new(1.0, t.focus), StrokeKind::Inside);
|
|
}
|
|
}
|
|
// Floating groups over the dock.
|
|
let mut float_groups: Vec<Group> = vec![];
|
|
for i in 0..self.ui.floating.len() {
|
|
if let Some((g, tabs)) = self.floating_window(&ctx, i, &info, &mut actions) {
|
|
tab_rects.push((g.path.clone(), tabs));
|
|
float_groups.push(g);
|
|
}
|
|
}
|
|
for a in actions {
|
|
match a {
|
|
DockAction::Activate(p) => {
|
|
if !self.ui.dock.activate(p)
|
|
&& let Some(f) = self.ui.floating.iter_mut().find(|f| f.panels.contains(&p))
|
|
{
|
|
f.active = f.panels.iter().position(|x| *x == p).unwrap_or(0);
|
|
}
|
|
}
|
|
DockAction::ToggleStacked(p) => {
|
|
self.ui.dock.toggle_stacked(p);
|
|
}
|
|
DockAction::Focus(p) => {
|
|
self.ui.focused = p;
|
|
// A click in (or on the tab of) another Composition viewer makes it active.
|
|
if let Some(id) = panels::viewers::id_of(p) {
|
|
panels::viewers::activate(self, &ctx, id);
|
|
}
|
|
}
|
|
DockAction::Close(p) => self.close_panel(p),
|
|
DockAction::PanelMenu(p, pos) => {
|
|
ctx.data_mut(|d| d.insert_temp(egui::Id::new("panel-menu"), (p, pos)));
|
|
}
|
|
DockAction::BeginDrag(p) => ctx.data_mut(|d| {
|
|
d.insert_temp(drag_id(), p);
|
|
}),
|
|
DockAction::ToggleLock(p) => {
|
|
if !self.ui.locked_tabs.remove(&p.id()) {
|
|
self.ui.locked_tabs.insert(p.id().to_string());
|
|
}
|
|
}
|
|
// The Timeline's comp tabs: show that comp / close its Timeline.
|
|
DockAction::ActivateDoc(p, id) => {
|
|
if !self.ui.dock.activate(p)
|
|
&& let Some(f) = self.ui.floating.iter_mut().find(|f| f.panels.contains(&p))
|
|
{
|
|
f.active = f.panels.iter().position(|x| *x == p).unwrap_or(0);
|
|
}
|
|
if self.session.state.active_comp.map(|c| c.0) != Some(id) {
|
|
let _ = crate::menus::invoke(self, &ctx, "comp.open", serde_json::json!({"comp": id}));
|
|
}
|
|
}
|
|
DockAction::CloseDoc(_, id) => {
|
|
let _ = crate::menus::invoke(self, &ctx, "comp.close", serde_json::json!({"comp": id}));
|
|
}
|
|
}
|
|
}
|
|
self.tab_drag(&ctx, &groups, &float_groups, &tab_rects);
|
|
panels::panel_menu_popup(self, ui);
|
|
}
|
|
|
|
/// One floating group: a window with a tab strip (drag the empty strip to move it, the
|
|
/// corner to resize it) and the active panel's body.
|
|
fn floating_window(
|
|
&mut self,
|
|
ctx: &egui::Context,
|
|
i: usize,
|
|
info: &dock::TabInfo,
|
|
actions: &mut Vec<DockAction>,
|
|
) -> Option<(Group, Vec<(PanelKind, Rect)>)> {
|
|
let t = self.tokens;
|
|
let f = self.ui.floating.get(i)?.clone();
|
|
let screen = ctx.content_rect();
|
|
let mut rect = Rect::from_min_size(pos2(f.rect[0], f.rect[1]), vec2(f.rect[2].max(160.0), f.rect[3].max(100.0)));
|
|
// Keep the strip reachable.
|
|
rect = rect.translate(vec2(
|
|
(screen.min.x - rect.min.x).max(0.0) + (screen.max.x - 40.0 - rect.min.x).min(0.0),
|
|
(screen.min.y - rect.min.y).max(0.0) + (screen.max.y - 30.0 - rect.min.y).min(0.0),
|
|
));
|
|
let id = egui::Id::new(("floating-panel", f.panels.first().map(|p| p.id()).unwrap_or_default()));
|
|
let mut group = None;
|
|
egui::Area::new(id).order(egui::Order::Middle).fixed_pos(rect.min).interactable(true).show(ctx, |ui| {
|
|
let (r, _) = ui.allocate_exact_size(rect.size(), Sense::hover());
|
|
ui.painter().rect_filled(r.expand(1.0), t.radius, Color32::from_black_alpha(90));
|
|
let strip = Rect::from_min_size(r.min, vec2(r.width(), t.tab_h));
|
|
// Move by the strip (tabs, registered later, take precedence over it).
|
|
let mv = ui.interact(strip, id.with("move"), Sense::drag());
|
|
self.auto.add(&format!("panel.float.{i}.move"), strip, "Move floating panel");
|
|
let mut nr = r;
|
|
if mv.dragged() {
|
|
nr = nr.translate(mv.drag_delta());
|
|
}
|
|
let g = Group {
|
|
path: format!("f{i}"),
|
|
rect: r,
|
|
content: Rect::from_min_max(pos2(r.min.x, r.min.y + t.tab_h), r.max),
|
|
panels: f.panels.clone(),
|
|
active: f.active.min(f.panels.len().saturating_sub(1)),
|
|
stacked: None,
|
|
};
|
|
let c = dock::draw_group_chrome(ui, &g, &t, &mut self.auto, info);
|
|
actions.extend(c.actions);
|
|
if let Some(p) = g.panels.get(g.active).copied() {
|
|
self.panel_body(ui, p, g.content);
|
|
}
|
|
ui.painter().rect_stroke(r, t.radius, Stroke::new(1.0, t.field_border), StrokeKind::Outside);
|
|
if g.panels.get(g.active) == Some(&self.ui.focused) {
|
|
ui.painter().rect_stroke(r, t.radius, Stroke::new(1.0, t.focus), StrokeKind::Inside);
|
|
}
|
|
// Resize from the corner.
|
|
let grip = Rect::from_min_max(r.max - vec2(14.0, 14.0), r.max);
|
|
let rs = ui.interact(grip, id.with("resize"), Sense::drag());
|
|
self.auto.add(&format!("panel.float.{i}.resize"), grip, "Resize floating panel");
|
|
for k in 0..3 {
|
|
let o = 4.0 + k as f32 * 4.0;
|
|
ui.painter().line_segment([pos2(r.max.x - o, r.max.y - 2.0), pos2(r.max.x - 2.0, r.max.y - o)], Stroke::new(1.0, t.text_faint));
|
|
}
|
|
if rs.hovered() || rs.dragged() {
|
|
ui.ctx().set_cursor_icon(egui::CursorIcon::ResizeNwSe);
|
|
}
|
|
if rs.dragged() {
|
|
nr = Rect::from_min_size(nr.min, (nr.size() + rs.drag_delta()).max(vec2(160.0, 100.0)));
|
|
}
|
|
if nr != r
|
|
&& let Some(fl) = self.ui.floating.get_mut(i)
|
|
{
|
|
fl.rect = [nr.min.x, nr.min.y, nr.width(), nr.height()];
|
|
}
|
|
group = Some((g, c.tabs));
|
|
});
|
|
group
|
|
}
|
|
|
|
/// A tab being dragged. Over a tab strip (or the middle of a group) it shows where the tab
|
|
/// will land: an insertion mark between two tabs (a drop there reorders a group's own tabs
|
|
/// too); near a group's edge, the half it will split off. On release it lands there (or
|
|
/// floats with Cmd/Ctrl or outside every group).
|
|
fn tab_drag(&mut self, ctx: &egui::Context, groups: &[Group], floats: &[Group], tab_rects: &[(String, Vec<(PanelKind, Rect)>)]) {
|
|
let Some(p) = ctx.data(|d| d.get_temp::<PanelKind>(drag_id())) else { return };
|
|
let t = self.tokens;
|
|
let Some(pos) = ctx.pointer_latest_pos() else { return };
|
|
// Floating windows are on top; then the docked groups.
|
|
let target = floats.iter().map(|g| (g, true)).chain(groups.iter().map(|g| (g, false))).find(|(g, _)| g.rect.contains(pos));
|
|
let float_drop = ctx.input(|i| i.modifiers.command);
|
|
let painter = ctx.layer_painter(egui::LayerId::new(egui::Order::Foreground, egui::Id::new("dock-drop")));
|
|
let plan: Option<DropPlan> = match target {
|
|
Some((g, is_float)) if !float_drop => {
|
|
let others: Vec<PanelKind> = g.panels.iter().copied().filter(|x| *x != p).collect();
|
|
let zone = if is_float { Zone::Center } else { dock::drop_zone(g.rect, t.tab_h, pos) };
|
|
match (others.first().copied(), zone) {
|
|
// The dragged tab alone in its group: nowhere to go here.
|
|
(None, _) => None,
|
|
(Some(first), Zone::Center) => {
|
|
let rects: Vec<(PanelKind, Rect)> = tab_rects
|
|
.iter()
|
|
.find(|(path, _)| *path == g.path)
|
|
.map(|(_, r)| r.iter().copied().filter(|(k, _)| *k != p).collect())
|
|
.unwrap_or_default();
|
|
let strip = Rect::from_min_size(g.rect.min, vec2(g.rect.width(), t.tab_h));
|
|
let before = if strip.contains(pos) {
|
|
// Before the first tab whose middle is right of the pointer.
|
|
rects.iter().find(|(_, r)| r.center().x > pos.x).map(|(k, _)| *k)
|
|
} else if g.panels.contains(&p) {
|
|
// The middle of its own group: it stays where it is.
|
|
Some(p)
|
|
} else {
|
|
// The middle of another group: right after its shown tab.
|
|
let shown = g.panels.get(g.active).and_then(|a| others.iter().position(|x| x == a)).unwrap_or(others.len());
|
|
others.get(shown + 1).copied()
|
|
};
|
|
if before == Some(p) {
|
|
None
|
|
} else {
|
|
let x = match before.and_then(|b| rects.iter().find(|(k, _)| *k == b)) {
|
|
Some((_, r)) => r.min.x - 4.0,
|
|
None => rects.last().map_or(strip.min.x + 8.0, |(_, r)| r.max.x + 4.0),
|
|
};
|
|
painter.rect_filled(strip.shrink2(vec2(2.0, 1.0)), t.radius, t.focus.gamma_multiply(0.12));
|
|
if !strip.contains(pos) {
|
|
painter.rect_stroke(g.rect, t.radius, Stroke::new(1.0, t.focus.gamma_multiply(0.6)), StrokeKind::Inside);
|
|
}
|
|
insertion_mark(&painter, x, strip, t.focus);
|
|
Some(DropPlan::Tab { anchor: first, before })
|
|
}
|
|
}
|
|
(Some(first), zone) => {
|
|
let shown = g.panels.get(g.active).copied().filter(|a| *a != p).unwrap_or(first);
|
|
let hr = dock::zone_rect(g.rect, zone).shrink(2.0);
|
|
painter.rect_filled(hr, t.radius, t.focus.gamma_multiply(0.25));
|
|
painter.rect_stroke(hr, t.radius, Stroke::new(2.0, t.focus), StrokeKind::Inside);
|
|
// Outline the whole group for the side zones, like AE's drop-zone frame.
|
|
painter.rect_stroke(g.rect, t.radius, Stroke::new(1.0, t.focus.gamma_multiply(0.6)), StrokeKind::Inside);
|
|
Some(DropPlan::Side { anchor: shown, zone })
|
|
}
|
|
}
|
|
}
|
|
_ => None,
|
|
};
|
|
// Ghost of the dragged tab.
|
|
let g = painter.layout_no_wrap(p.title().to_string(), crate::theme::Tokens::ui(12.0), Color32::WHITE);
|
|
let gr = Rect::from_min_size(pos + vec2(12.0, 10.0), g.size() + vec2(16.0, 8.0));
|
|
painter.rect_filled(gr, 4.0, t.focus.gamma_multiply(0.85));
|
|
painter.galley(gr.min + vec2(8.0, 4.0), g, Color32::WHITE);
|
|
ctx.set_cursor_icon(egui::CursorIcon::Grabbing);
|
|
ctx.request_repaint();
|
|
if ctx.input(|i| i.pointer.any_released()) {
|
|
ctx.data_mut(|d| d.remove::<PanelKind>(drag_id()));
|
|
let ok = match plan {
|
|
Some(DropPlan::Tab { anchor, before }) => self.edit_layout(|l| l.insert_tab(p, anchor, before)),
|
|
Some(DropPlan::Side { anchor, zone }) => self.edit_layout(|l| l.dock(p, anchor, zone)),
|
|
None if float_drop || target.is_none() => {
|
|
let r = [pos.x - 40.0, pos.y - 12.0, 420.0, 320.0];
|
|
self.edit_layout(|l| l.float(p, r))
|
|
}
|
|
None => false,
|
|
};
|
|
if ok {
|
|
self.ui.focused = p;
|
|
self.show_panel(p);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Where a dragged tab lands.
|
|
enum DropPlan {
|
|
/// A tab in the group of `anchor`, before `before` (last when `None`).
|
|
Tab { anchor: PanelKind, before: Option<PanelKind> },
|
|
/// A new group split off one side of `anchor`'s group.
|
|
Side { anchor: PanelKind, zone: Zone },
|
|
}
|
|
|
|
/// The insertion mark of a tab drag: a bar between two tabs at `x`, with caps.
|
|
fn insertion_mark(painter: &egui::Painter, x: f32, strip: Rect, col: Color32) {
|
|
let (y0, y1) = (strip.min.y + 3.0, strip.max.y - 2.0);
|
|
painter.line_segment([pos2(x, y0), pos2(x, y1)], Stroke::new(2.5, col));
|
|
for (y, d) in [(y0, 1.0), (y1, -1.0)] {
|
|
painter.add(egui::Shape::convex_polygon(vec![pos2(x - 4.5, y), pos2(x + 4.5, y), pos2(x, y + 5.0 * d)], col, Stroke::NONE));
|
|
}
|
|
}
|
|
|
|
/// A new floating group's default rect near the centre of `screen`.
|
|
pub fn default_float_rect(screen: Rect) -> [f32; 4] {
|
|
let c = screen.center();
|
|
[c.x - 210.0, c.y - 160.0, 420.0, 320.0]
|
|
}
|