Merge pull request #277 from storytold/fix/269-271-272-ui
UI: long menus scroll, panels get scroll bars, Learn closes when leaving it (#269, #271, #272)
This commit is contained in:
commit
fbc4080f53
|
|
@ -383,16 +383,21 @@ impl EffectcraftApp {
|
|||
}
|
||||
|
||||
pub fn set_workspace(&mut self, name: &str) {
|
||||
self.ui.workspace = name.to_string();
|
||||
self.ui.dock = self.ui.saved_workspaces.get(name).cloned().unwrap_or_else(|| dock::workspace(name));
|
||||
self.ui.floating = self.ui.saved_floating.get(name).cloned().unwrap_or_else(|| dock::workspace_floating(name));
|
||||
self.ui.maximized = None;
|
||||
// Learn: the Home screen's Learn tab (tutorials) in the Composition panel.
|
||||
// Learn: the Home screen's Learn tab (tutorials) in the Composition panel. Leaving it for
|
||||
// another workspace closes the Home screen again: it stayed over every workspace until a
|
||||
// project was created (#272).
|
||||
if name == "Learn" {
|
||||
self.ui.start_screen = true;
|
||||
self.ui.home_learn = true;
|
||||
self.ui.home_templates = false;
|
||||
} else if self.ui.workspace == "Learn" {
|
||||
self.ui.start_screen = false;
|
||||
self.ui.home_learn = false;
|
||||
}
|
||||
self.ui.workspace = name.to_string();
|
||||
self.ui.dock = self.ui.saved_workspaces.get(name).cloned().unwrap_or_else(|| dock::workspace(name));
|
||||
self.ui.floating = self.ui.saved_floating.get(name).cloned().unwrap_or_else(|| dock::workspace_floating(name));
|
||||
self.ui.maximized = None;
|
||||
}
|
||||
|
||||
/// Built-in workspaces followed by the saved ones (Window ▸ Workspace ▸ Save as New Workspace).
|
||||
|
|
@ -1543,6 +1548,7 @@ impl eframe::App for EffectcraftApp {
|
|||
self.apply_gpu_failure(ctx);
|
||||
if !self.styled {
|
||||
theme::install(ctx, &self.tokens);
|
||||
fit_window(ctx);
|
||||
self.styled = true;
|
||||
ctx.request_repaint();
|
||||
} else {
|
||||
|
|
@ -1598,6 +1604,39 @@ pub(crate) fn tick_playback(app: &mut EffectcraftApp, ctx: &egui::Context, scale
|
|||
app.advance_playback(ctx, scale);
|
||||
}
|
||||
|
||||
/// Maximize a first window that doesn't fit the screen, which fits it to the space beside the
|
||||
/// taskbar or dock. The 1680 × 1020 window on a smaller screen is only shrunk to the monitor's
|
||||
/// size, so with its title bar it ran under the taskbar and cut menus off at the bottom of the
|
||||
/// screen (#269).
|
||||
fn fit_window(ctx: &egui::Context) {
|
||||
let (monitor, window) = ctx.input(|i| (i.viewport().monitor_size, i.viewport().outer_rect.or(i.viewport().inner_rect)));
|
||||
if let (Some(monitor), Some(window)) = (monitor, window)
|
||||
&& overflows_screen(window.size(), monitor)
|
||||
{
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::Maximized(true));
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a window `size` leaves no room on a `monitor` for a taskbar or dock (points).
|
||||
fn overflows_screen(size: egui::Vec2, monitor: egui::Vec2) -> bool {
|
||||
const TASKBAR: f32 = 64.0;
|
||||
size.x > monitor.x || size.y > monitor.y - TASKBAR
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod fit_window_tests {
|
||||
use super::overflows_screen;
|
||||
use egui::vec2;
|
||||
|
||||
#[test]
|
||||
fn a_window_taller_than_the_screen_beside_its_taskbar_overflows() {
|
||||
// The default window on a 1366 × 768 laptop (shrunk to the monitor) and on 1080p.
|
||||
assert!(overflows_screen(vec2(1366.0, 799.0), vec2(1366.0, 768.0)));
|
||||
assert!(overflows_screen(vec2(1680.0, 1051.0), vec2(1920.0, 1080.0)));
|
||||
assert!(!overflows_screen(vec2(1680.0, 1051.0), vec2(2560.0, 1440.0)));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod gpu_failure_tests {
|
||||
use super::*;
|
||||
|
|
|
|||
|
|
@ -1537,7 +1537,7 @@ pub fn menu_bar(app: &mut EffectcraftApp, ui: &mut egui::Ui) {
|
|||
if let MenuNode::Submenu { label, children } = node {
|
||||
let r = ui.menu_button(crate::i18n::label(app, "", label), |ui| {
|
||||
ui.set_min_width(if label == "Effect" { 200.0 } else { 280.0 });
|
||||
menu_nodes(app, ui, children, &mut clicked);
|
||||
crate::widgets::menu_scroll(ui, |ui| menu_nodes(app, ui, children, &mut clicked));
|
||||
});
|
||||
app.auto.add(&format!("menu.{label}"), r.response.rect, label);
|
||||
}
|
||||
|
|
@ -1604,12 +1604,7 @@ fn menu_nodes(app: &mut EffectcraftApp, ui: &mut egui::Ui, nodes: &[MenuNode], c
|
|||
let shown = crate::i18n::submenu(app, label, effectcraft_engine::menus::submenu_label(&app.session, label, &dyn_ctx(&ws, &[])));
|
||||
ui.menu_button((gutter(false), shown.as_str()), |ui| {
|
||||
ui.set_min_width(if children.len() > 30 { 200.0 } else { 240.0 });
|
||||
// Long submenus (Blending Mode, effect categories) scroll instead of running
|
||||
// off the screen. Others show whole: egui sizes a new submenu from a default
|
||||
// 400 pt area, so without a minimum a longer one (Window ▸ Workspace) got
|
||||
// stuck at that height with its last entries scrolled out of view (#191).
|
||||
let max_h = ui.ctx().content_rect().height() - 40.0;
|
||||
egui::ScrollArea::vertical().max_height(max_h).min_scrolled_height(max_h).show(ui, |ui| menu_nodes(app, ui, children, clicked));
|
||||
crate::widgets::menu_scroll(ui, |ui| menu_nodes(app, ui, children, clicked));
|
||||
});
|
||||
}
|
||||
MenuNode::Item(e) => {
|
||||
|
|
|
|||
|
|
@ -198,16 +198,12 @@ fn graph(app: &mut EffectcraftApp, ui: &mut egui::Ui, g: Rect, curve: &mut EaseC
|
|||
fn grid(app: &mut EffectcraftApp, ui: &mut egui::Ui, list: Rect, selected: Option<&str>) -> Option<(String, EaseCurve)> {
|
||||
let t = app.tokens;
|
||||
let p = ui.painter().with_clip_rect(list);
|
||||
let scroll_id = egui::Id::new("easePresets.scroll");
|
||||
let mut scroll: f32 = ui.ctx().data(|d| d.get_temp(scroll_id).unwrap_or(0.0));
|
||||
if ui.rect_contains_pointer(list) {
|
||||
scroll = (scroll - ui.input(|i| i.smooth_scroll_delta.y)).max(0.0);
|
||||
}
|
||||
let scroll = widgets::PanelScroll::begin(ui, egui::Id::new("ease-presets-scroll"), list);
|
||||
let cols = (((list.width() + GAP) / (TILE.x + GAP)).floor() as usize).max(1);
|
||||
let (mut clicked, mut count) = (None, 0);
|
||||
for (n, (name, curve, built_in)) in ease_presets::presets(&app.session).enumerate() {
|
||||
count = n + 1;
|
||||
let r = Rect::from_min_size(list.min + vec2((n % cols) as f32 * (TILE.x + GAP), (n / cols) as f32 * (TILE.y + GAP) - scroll), TILE);
|
||||
let r = Rect::from_min_size(list.min + vec2((n % cols) as f32 * (TILE.x + GAP), (n / cols) as f32 * (TILE.y + GAP) - scroll.offset), TILE);
|
||||
if !r.intersects(list) {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -242,8 +238,6 @@ fn grid(app: &mut EffectcraftApp, ui: &mut egui::Ui, list: Rect, selected: Optio
|
|||
clicked = Some((name.to_string(), curve));
|
||||
}
|
||||
}
|
||||
let content = count.div_ceil(cols) as f32 * (TILE.y + GAP);
|
||||
scroll = scroll.min((content - list.height()).max(0.0));
|
||||
ui.ctx().data_mut(|d| d.insert_temp(scroll_id, scroll));
|
||||
scroll.end(ui, &mut app.auto, "easePresets.scroll", count.div_ceil(cols) as f32 * (TILE.y + GAP), &t);
|
||||
clicked
|
||||
}
|
||||
|
|
|
|||
|
|
@ -182,10 +182,27 @@ fn prop_row(
|
|||
if prop.is_animated() {
|
||||
key_navigator(app, ui, p, layer, prop, ectx, r, actions);
|
||||
}
|
||||
let name_clip = Rect::from_min_max(pos2(swr.max.x + 6.0, r.min.y), pos2((r.min.x + r.width() * 0.48).max(swr.max.x + 124.0) - 4.0, r.max.y));
|
||||
p.with_clip_rect(name_clip.intersect(p.clip_rect())).text(pos2(swr.max.x + 6.0, cy), Align2::LEFT_CENTER, &prop.name, Tokens::ui(12.0), t.text);
|
||||
let vx = (r.min.x + r.width() * 0.48).max(swr.max.x + 130.0);
|
||||
let value = ectx.value(layer, prop);
|
||||
let vx = (r.min.x + r.width() * 0.48).max(swr.max.x + 130.0);
|
||||
// A vector's values end before the keyframe navigator: in a narrow panel they move left over
|
||||
// the name (cut short) instead of running under the navigator (#271).
|
||||
let vx = match &value {
|
||||
Value::Vec2(_) | Value::Vec3(_) => {
|
||||
let point = if matches!(prop.ui, ParamUi::Point | ParamUi::Point3) { 22.0 } else { 0.0 };
|
||||
let fields: f32 = value
|
||||
.components()
|
||||
.iter()
|
||||
.take(shown_dims(layer, prop, &value))
|
||||
.map(|v| p.layout_no_wrap(format!("{v:.1}"), Tokens::ui(12.0), t.hot_text).size().x + 12.0)
|
||||
.sum();
|
||||
let right = r.max.x - if prop.is_animated() { 52.0 } else { 8.0 };
|
||||
vx.min(right - point - fields + 8.0).max(swr.max.x + 60.0)
|
||||
}
|
||||
_ => vx,
|
||||
};
|
||||
let name_clip =
|
||||
Rect::from_min_max(pos2(swr.max.x + 6.0, r.min.y), pos2(((r.min.x + r.width() * 0.48).max(swr.max.x + 124.0) - 4.0).min(vx - 6.0), r.max.y));
|
||||
p.with_clip_rect(name_clip.intersect(p.clip_rect())).text(pos2(swr.max.x + 6.0, cy), Align2::LEFT_CENTER, &prop.name, Tokens::ui(12.0), t.text);
|
||||
let merge = format!("ec-{uid}");
|
||||
let set =
|
||||
|actions: &mut Actions, v: serde_json::Value| actions.push(("prop.set".into(), json!({"layer": layer.id.0, "prop": uid, "value": v, "merge": merge})));
|
||||
|
|
@ -264,7 +281,7 @@ fn prop_row(
|
|||
}
|
||||
Value::Vec2(_) | Value::Vec3(_) => {
|
||||
let c = value.components();
|
||||
let n = if prop.shown_dims > 0 && !(layer.is_3d() && c.len() == 3) { prop.shown_dims as usize } else { c.len() };
|
||||
let n = shown_dims(layer, prop, &value);
|
||||
let mut x = vx;
|
||||
if matches!(prop.ui, ParamUi::Point | ParamUi::Point3) {
|
||||
let cr = Rect::from_center_size(pos2(x + 8.0, cy), vec2(18.0, 18.0));
|
||||
|
|
@ -380,6 +397,12 @@ fn prop_row(
|
|||
}
|
||||
}
|
||||
|
||||
/// How many of a vector parameter's values show (a 3D point's Z only on a 3D layer).
|
||||
fn shown_dims(layer: &Layer, prop: &Property, value: &Value) -> usize {
|
||||
let n = value.components().len();
|
||||
if prop.shown_dims > 0 && !(layer.is_3d() && n == 3) { prop.shown_dims as usize } else { n }
|
||||
}
|
||||
|
||||
/// Colour of a gradient at `f` (colour stops only).
|
||||
fn gradient_at(g: &effectcraft_engine::keyframe::Gradient, f: f64) -> Color32 {
|
||||
let stops = &g.colors;
|
||||
|
|
@ -906,12 +929,8 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|||
p.text(pos2(hdr.min.x + 10.0, hdr.center().y), Align2::LEFT_CENTER, format!("{} • {}", comp_name, layer.name), Tokens::ui(11.5), t.text_dim);
|
||||
p.line_segment([hdr.left_bottom(), hdr.right_bottom()], Stroke::new(1.0, t.separator));
|
||||
let body = Rect::from_min_max(pos2(rect.min.x, hdr.max.y), rect.max);
|
||||
let scroll_id = egui::Id::new("ec-scroll");
|
||||
let mut scroll: f32 = ctx.data(|d| d.get_temp(scroll_id).unwrap_or(0.0));
|
||||
if ui.rect_contains_pointer(body) {
|
||||
scroll = (scroll - ui.input(|i| i.smooth_scroll_delta.y)).max(0.0);
|
||||
}
|
||||
let mut y = body.min.y + 4.0 - scroll;
|
||||
let scroll = widgets::PanelScroll::begin(ui, egui::Id::new("ec-scroll"), body);
|
||||
let mut y = body.min.y + 4.0 - scroll.offset;
|
||||
let mut actions: Actions = vec![];
|
||||
let bp = p.with_clip_rect(body);
|
||||
// Rows scroll under the header: clip their widgets (and hit tests) to the body.
|
||||
|
|
@ -1074,9 +1093,8 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|||
actions.extend(crate::menus::menu_contents(app, ui, "Effect"));
|
||||
});
|
||||
ui.set_clip_rect(panel_clip);
|
||||
let content_h = y + scroll - body.min.y;
|
||||
scroll = scroll.min((content_h - body.height()).max(0.0));
|
||||
ctx.data_mut(|d| d.insert_temp(scroll_id, scroll));
|
||||
let content_h = y + scroll.offset - body.min.y;
|
||||
scroll.end(ui, &mut app.auto, "effectControls.scroll", content_h, &t);
|
||||
// Drop effects here.
|
||||
if let Some(payload) = egui::DragAndDrop::payload::<crate::panels::DragPayload>(&ctx)
|
||||
&& ui.rect_contains_pointer(rect)
|
||||
|
|
|
|||
|
|
@ -313,14 +313,10 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|||
let view = app.ui.effects_view.clone();
|
||||
let list = Rect::from_min_max(pos2(rect.min.x, sr.max.y + 6.0), rect.max);
|
||||
let lp = p.with_clip_rect(list);
|
||||
let scroll_id = egui::Id::new("fx-scroll");
|
||||
let mut scroll: f32 = ctx.data(|d| d.get_temp(scroll_id).unwrap_or(0.0));
|
||||
if ui.rect_contains_pointer(list) {
|
||||
scroll = (scroll - ui.input(|i| i.smooth_scroll_delta.y)).max(0.0);
|
||||
}
|
||||
let scroll = widgets::PanelScroll::begin(ui, egui::Id::new("fx-scroll"), list);
|
||||
let query = q.trim().to_lowercase();
|
||||
let searching = !query.is_empty();
|
||||
let mut y = list.min.y - scroll;
|
||||
let mut y = list.min.y - scroll.offset;
|
||||
let mut apply: Option<Apply> = None;
|
||||
let reg = effectcraft_engine::effects::all();
|
||||
let keep =
|
||||
|
|
@ -397,12 +393,11 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|||
}
|
||||
}
|
||||
}
|
||||
if y == list.min.y - scroll && searching {
|
||||
if y == list.min.y - scroll.offset && searching {
|
||||
lp.text(pos2(list.center().x, list.min.y + 24.0), Align2::CENTER_CENTER, "No matching effects or presets", Tokens::ui(12.0), t.text_faint);
|
||||
}
|
||||
let content = y + scroll - list.min.y;
|
||||
scroll = scroll.min((content - list.height()).max(0.0));
|
||||
ctx.data_mut(|d| d.insert_temp(scroll_id, scroll));
|
||||
let content = y + scroll.offset - list.min.y;
|
||||
scroll.end(ui, &mut app.auto, "effects.scroll", content, &t);
|
||||
if let Some(DragPayload::Effect(id)) = egui::DragAndDrop::payload::<DragPayload>(&ctx).as_deref()
|
||||
&& let Some(pos) = ctx.pointer_hover_pos()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,14 +2,15 @@
|
|||
|
||||
use egui::{Align2, Color32, Rect, pos2, vec2};
|
||||
|
||||
use crate::EffectcraftApp;
|
||||
use crate::theme::Tokens;
|
||||
use crate::{EffectcraftApp, widgets};
|
||||
|
||||
pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
let t = app.tokens;
|
||||
let p = ui.painter().with_clip_rect(rect);
|
||||
let x0 = rect.min.x + 12.0;
|
||||
let mut y = rect.min.y + 14.0;
|
||||
let scroll = widgets::PanelScroll::begin(ui, egui::Id::new("info-scroll"), rect);
|
||||
let mut y = rect.min.y + 14.0 - scroll.offset;
|
||||
if app.pointer_comp.is_some() {
|
||||
// GPU frames: read the shown frame back for sampling.
|
||||
app.viewer_pixels();
|
||||
|
|
@ -60,6 +61,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|||
y += 82.0;
|
||||
p.line_segment([pos2(rect.min.x + 8.0, y), pos2(rect.max.x - 8.0, y)], egui::Stroke::new(1.0, t.separator));
|
||||
y += 14.0;
|
||||
let mut bottom = y;
|
||||
let comp = app.session.active_comp_arc();
|
||||
if let Some(c) = comp {
|
||||
let sel: Vec<String> = app.session.state.selected_layers.iter().filter_map(|id| c.layer(*id)).map(|l| l.name.clone()).collect();
|
||||
|
|
@ -121,5 +123,8 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|||
let line = one.unwrap_or_else(|| format!("{} keyframes selected", sel.len()));
|
||||
p.text(pos2(x0, ly + 18.0), Align2::LEFT_CENTER, line, Tokens::ui(11.5), t.text_dim);
|
||||
}
|
||||
bottom = ly + 30.0;
|
||||
}
|
||||
let content = bottom - (rect.min.y - scroll.offset);
|
||||
scroll.end(ui, &mut app.auto, "info.scroll", content, &t);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@ use serde_json::{Value, json};
|
|||
|
||||
use super::DragPayload;
|
||||
use super::panel_kit as kit;
|
||||
use crate::EffectcraftApp;
|
||||
use crate::theme::Tokens;
|
||||
use crate::{EffectcraftApp, widgets};
|
||||
|
||||
// ---------------------------------------------------------------- Media Browser
|
||||
|
||||
|
|
@ -159,19 +159,12 @@ pub fn media_browser(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|||
let tile = vec2(140.0, 112.0);
|
||||
let cols = ((list.width() - 8.0) / tile.x).floor().max(1.0) as usize;
|
||||
let mut budget = 2u32;
|
||||
let scroll_id = egui::Id::new("mb-scroll");
|
||||
let mut scroll: f32 = ctx.data(|d| d.get_temp(scroll_id).unwrap_or(0.0));
|
||||
if ui.rect_contains_pointer(list) {
|
||||
scroll -= ctx.input(|i| i.smooth_scroll_delta.y);
|
||||
}
|
||||
let rows = entries.len().div_ceil(cols);
|
||||
scroll = scroll.clamp(0.0, (rows as f32 * tile.y - list.height() + 8.0).max(0.0));
|
||||
ctx.data_mut(|d| d.insert_temp(scroll_id, scroll));
|
||||
let scroll = widgets::PanelScroll::begin(ui, egui::Id::new("mb-scroll"), list);
|
||||
let lp = p.with_clip_rect(list);
|
||||
let mut actions: Vec<(&str, Value)> = vec![];
|
||||
for (i, e) in entries.iter().enumerate() {
|
||||
let r = Rect::from_min_size(
|
||||
pos2(list.min.x + 8.0 + (i % cols) as f32 * tile.x, list.min.y + 8.0 + (i / cols) as f32 * tile.y - scroll),
|
||||
pos2(list.min.x + 8.0 + (i % cols) as f32 * tile.x, list.min.y + 8.0 + (i / cols) as f32 * tile.y - scroll.offset),
|
||||
tile - vec2(8.0, 8.0),
|
||||
);
|
||||
if !r.intersects(list) {
|
||||
|
|
@ -232,6 +225,7 @@ pub fn media_browser(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|||
if entries.is_empty() {
|
||||
lp.text(list.center(), Align2::CENTER_CENTER, "Empty folder", Tokens::ui(12.0), t.text_faint);
|
||||
}
|
||||
scroll.end(ui, &mut app.auto, "mediaBrowser.scroll", entries.len().div_ceil(cols) as f32 * tile.y + 8.0, &t);
|
||||
if budget == 0 {
|
||||
ctx.request_repaint();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,14 +23,10 @@ pub fn preview(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|||
let p = ui.painter().with_clip_rect(rect);
|
||||
// The panel scrolls when it is shorter than its controls (the default workspace shows just
|
||||
// the transport row, as in After Effects).
|
||||
let scroll_id = egui::Id::new("preview-scroll");
|
||||
let mut scroll: f32 = ctx.data(|d| d.get_temp(scroll_id).unwrap_or(0.0));
|
||||
if ui.rect_contains_pointer(rect) {
|
||||
scroll = (scroll - ui.input(|i| i.smooth_scroll_delta.y)).max(0.0);
|
||||
}
|
||||
let scroll = widgets::PanelScroll::begin(ui, egui::Id::new("preview-scroll"), rect);
|
||||
let panel_clip = ui.clip_rect();
|
||||
ui.set_clip_rect(rect.intersect(panel_clip));
|
||||
let y = rect.min.y + 12.0 - scroll;
|
||||
let y = rect.min.y + 12.0 - scroll.offset;
|
||||
let bw = 30.0;
|
||||
let total = bw * 5.0 + 16.0;
|
||||
let mut x = rect.center().x - total / 2.0;
|
||||
|
|
@ -231,17 +227,8 @@ pub fn preview(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|||
p.text(pos2(bar.min.x, bar.max.y + 12.0), Align2::LEFT_CENTER, format!("{cached} / {total} frames cached{status}"), Tokens::ui(11.0), t.text_faint);
|
||||
}
|
||||
ui.set_clip_rect(panel_clip);
|
||||
let content = yy + 30.0 - (rect.min.y - scroll);
|
||||
let max = (content - rect.height()).max(0.0);
|
||||
scroll = scroll.min(max);
|
||||
ctx.data_mut(|d| d.insert_temp(scroll_id, scroll));
|
||||
if max > 0.0 {
|
||||
// A thin scroll indicator on the right edge.
|
||||
let h = (rect.height() * rect.height() / content).max(12.0);
|
||||
let top = rect.min.y + (rect.height() - h) * (scroll / max);
|
||||
p.rect_filled(Rect::from_min_size(pos2(rect.max.x - 5.0, top), vec2(3.0, h)), 1.5, t.text_faint.gamma_multiply(0.6));
|
||||
app.auto.add("preview.scroll", Rect::from_min_size(pos2(rect.max.x - 6.0, rect.min.y), vec2(6.0, rect.height())), "Preview scroll");
|
||||
}
|
||||
let content = yy + 30.0 - (rect.min.y - scroll.offset);
|
||||
scroll.end(ui, &mut app.auto, "preview.scroll", content, &t);
|
||||
}
|
||||
|
||||
fn fmt_rate(r: f64) -> String {
|
||||
|
|
@ -268,28 +255,21 @@ fn dropdown_row(app: &mut EffectcraftApp, ui: &mut egui::Ui, r: Rect, key: &str,
|
|||
ui.data_mut(|d| d.insert_temp(pid.with("open"), !open));
|
||||
}
|
||||
app.auto.add(&format!("preview.{key}"), r, text);
|
||||
let open: bool = ui.data(|d| d.get_temp(pid.with("open")).unwrap_or(false));
|
||||
if !open {
|
||||
if !widgets::popup_is_open(ui, pid) {
|
||||
return None;
|
||||
}
|
||||
let mut chosen = None;
|
||||
let area = egui::Area::new(pid.with("area")).order(egui::Order::Foreground).fixed_pos(r.left_bottom() + vec2(0.0, 2.0)).show(ui.ctx(), |ui| {
|
||||
egui::Frame::popup(ui.style()).show(ui, |ui| {
|
||||
ui.set_min_width(r.width().max(140.0));
|
||||
for (i, label) in items.iter().enumerate() {
|
||||
let resp = ui.selectable_label(current == Some(i), label.as_str());
|
||||
app.auto.add(&format!("preview.{key}.{i}"), resp.rect, label);
|
||||
if resp.clicked() {
|
||||
chosen = Some(i);
|
||||
}
|
||||
widgets::popup_list(ui, pid, r.left_bottom() + vec2(0.0, 2.0), r, items.len(), 0, |ui| {
|
||||
ui.set_min_width(r.width().max(140.0));
|
||||
let mut chosen = None;
|
||||
for (i, label) in items.iter().enumerate() {
|
||||
let resp = ui.selectable_label(current == Some(i), label.as_str());
|
||||
app.auto.add(&format!("preview.{key}.{i}"), resp.rect, label);
|
||||
if resp.clicked() {
|
||||
chosen = Some(i);
|
||||
}
|
||||
});
|
||||
});
|
||||
let outside = widgets::pressed_outside(ui.ctx(), &area.response) && !r.contains(ui.input(|i| i.pointer.interact_pos()).unwrap_or_default());
|
||||
if chosen.is_some() || outside || ui.input(|i| i.key_pressed(egui::Key::Escape)) {
|
||||
ui.data_mut(|d| d.insert_temp(pid.with("open"), false));
|
||||
}
|
||||
chosen
|
||||
}
|
||||
chosen
|
||||
})
|
||||
}
|
||||
|
||||
/// Audio panel: L/R VU meters (dBFS, 0 to -48) with peak hold and clip indicators, fed by the
|
||||
|
|
@ -389,12 +369,24 @@ pub fn history(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|||
let nodes = app.session.history_tree();
|
||||
let row_h = 22.0;
|
||||
let cur = nodes.iter().position(|n| n.current).unwrap_or(0);
|
||||
// Keep the current state in view.
|
||||
let visible = ((rect.height() - 12.0) / row_h).max(1.0) as usize;
|
||||
let first = (cur + 2).saturating_sub(visible).min(nodes.len().saturating_sub(visible));
|
||||
let mut y = rect.min.y + 6.0;
|
||||
let scroll_id = egui::Id::new("history-scroll");
|
||||
let mut scroll = widgets::PanelScroll::begin(ui, scroll_id, rect);
|
||||
// The current state scrolls into view when it changes (undo, redo, a new step).
|
||||
if ui.data(|d| d.get_temp::<usize>(scroll_id.with("current"))) != Some(cur) {
|
||||
ui.data_mut(|d| d.insert_temp(scroll_id.with("current"), cur));
|
||||
let top = cur as f32 * row_h;
|
||||
if top < scroll.offset {
|
||||
scroll.offset = top;
|
||||
} else if top + row_h + 12.0 > scroll.offset + rect.height() {
|
||||
scroll.offset = top + row_h + 12.0 - rect.height();
|
||||
}
|
||||
}
|
||||
// Only the rows in view are drawn.
|
||||
let first = ((scroll.offset / row_h) as usize).min(nodes.len());
|
||||
let mut y = rect.min.y + 6.0 - (scroll.offset - first as f32 * row_h);
|
||||
let visible = (rect.height() / row_h) as usize + 2;
|
||||
let mut jump: Option<usize> = None;
|
||||
for n in nodes.iter().skip(first).take(visible + 1) {
|
||||
for n in nodes.iter().skip(first).take(visible) {
|
||||
let r = Rect::from_min_size(pos2(rect.min.x, y), vec2(rect.width(), row_h));
|
||||
y += row_h;
|
||||
let resp = ui.interact(r, egui::Id::new(("hist", n.id.as_str())), Sense::click());
|
||||
|
|
@ -417,6 +409,7 @@ pub fn history(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|||
jump = Some(n.index);
|
||||
}
|
||||
}
|
||||
scroll.end(ui, &mut app.auto, "history.scroll", nodes.len() as f32 * row_h + 12.0, &t);
|
||||
if let Some(i) = jump {
|
||||
let ctx = ui.ctx().clone();
|
||||
if let Err(e) = crate::menus::invoke(app, &ctx, "edit.history.goto", serde_json::json!({"index": i})) {
|
||||
|
|
@ -429,13 +422,17 @@ pub fn markers(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|||
let t = app.tokens;
|
||||
let p = ui.painter().with_clip_rect(rect);
|
||||
let Some(c) = app.session.active_comp_arc() else { return };
|
||||
let mut y = rect.min.y + 8.0;
|
||||
let scroll = widgets::PanelScroll::begin(ui, egui::Id::new("markers-scroll"), rect);
|
||||
let mut y = rect.min.y + 8.0 - scroll.offset;
|
||||
if c.markers.is_empty() {
|
||||
p.text(rect.center(), Align2::CENTER_CENTER, "No composition markers", Tokens::ui(12.0), t.text_faint);
|
||||
}
|
||||
for m in &c.markers {
|
||||
let r = Rect::from_min_size(pos2(rect.min.x, y), vec2(rect.width(), 22.0));
|
||||
y += 22.0;
|
||||
if !r.intersects(rect) {
|
||||
continue;
|
||||
}
|
||||
let tc = crate::panels::timecode(&app.session, &c, m.time);
|
||||
p.text(pos2(r.min.x + 12.0, r.center().y), Align2::LEFT_CENTER, tc, Tokens::mono(11.5), t.timecode);
|
||||
p.text(pos2(r.min.x + 120.0, r.center().y), Align2::LEFT_CENTER, &m.comment, Tokens::ui(12.0), t.text);
|
||||
|
|
@ -443,4 +440,6 @@ pub fn markers(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|||
app.session.set_time(m.time);
|
||||
}
|
||||
}
|
||||
let content = y + 8.0 - (rect.min.y - scroll.offset);
|
||||
scroll.end(ui, &mut app.auto, "markers.scroll", content, &t);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -175,12 +175,6 @@ fn fit(r: Rect, w: f32, h: f32) -> Rect {
|
|||
Rect::from_center_size(r.center(), vec2(w * s, h * s))
|
||||
}
|
||||
|
||||
/// A scroll bar thumb's length on a `track` long: the share of `content` in view, at least 16
|
||||
/// points but never longer than the track (a panel only a few points tall still draws, #231).
|
||||
fn thumb_len(track: f32, visible: f32, content: f32) -> f32 {
|
||||
(track * visible / content).max(16.0).min(track).max(0.0)
|
||||
}
|
||||
|
||||
/// An inline text edit in progress: (item, field `name`|`comment`, text).
|
||||
type Editing = (u64, String, String);
|
||||
fn edit_id() -> egui::Id {
|
||||
|
|
@ -345,18 +339,6 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|||
};
|
||||
app.ui.project_hscroll = (app.ui.project_hscroll - d).clamp(0.0, overflow);
|
||||
}
|
||||
if overflow > 0.0 {
|
||||
let track = Rect::from_min_max(pos2(opt_x0, list.max.y - 5.0), pos2(rect.max.x - 4.0, list.max.y - 1.0));
|
||||
let tw = thumb_len(track.width(), track.width(), track.width() + overflow);
|
||||
let thumb = Rect::from_min_size(pos2(track.min.x + (track.width() - tw) * (hscroll / overflow), track.min.y), vec2(tw, track.height()));
|
||||
let sresp = ui.interact(track.expand2(vec2(0.0, 2.0)), egui::Id::new("proj-hscroll"), Sense::drag());
|
||||
p.rect_filled(track, 2.0, t.field_bg);
|
||||
p.rect_filled(thumb, 2.0, if sresp.hovered() || sresp.dragged() { t.text_dim } else { t.text_faint });
|
||||
app.auto.add("project.hscroll", track, &format!("{hscroll}/{overflow}"));
|
||||
if sresp.dragged() {
|
||||
app.ui.project_hscroll = (hscroll + sresp.drag_delta().x * overflow / (track.width() - tw).max(1.0)).clamp(0.0, overflow);
|
||||
}
|
||||
}
|
||||
let rows = visible_rows(app);
|
||||
// Enter renames the selected item (Project panel focused, not typing).
|
||||
if app.ui.focused == crate::dock::PanelKind::Project
|
||||
|
|
@ -401,17 +383,6 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|||
}
|
||||
app.ui.project_scroll = app.ui.project_scroll.clamp(0.0, max_scroll);
|
||||
let scroll = app.ui.project_scroll;
|
||||
if max_scroll > 0.0 {
|
||||
let track = Rect::from_min_max(pos2(rect.max.x - 6.0, list.min.y + 1.0), pos2(rect.max.x - 2.0, list.max.y - 7.0));
|
||||
let th = thumb_len(track.height(), list.height(), content_h);
|
||||
let thumb = Rect::from_min_size(pos2(track.min.x, track.min.y + (track.height() - th) * (scroll / max_scroll)), vec2(track.width(), th));
|
||||
let vresp = ui.interact(track.expand2(vec2(2.0, 0.0)), egui::Id::new("proj-vscroll"), Sense::drag());
|
||||
p.rect_filled(thumb, 2.0, if vresp.hovered() || vresp.dragged() { t.text_dim } else { t.text_faint });
|
||||
app.auto.add("project.vscroll", track, &format!("{scroll}/{max_scroll}"));
|
||||
if vresp.dragged() {
|
||||
app.ui.project_scroll = (scroll + vresp.drag_delta().y * max_scroll / (track.height() - th).max(1.0)).clamp(0.0, max_scroll);
|
||||
}
|
||||
}
|
||||
let first = ((scroll / ROW_H).floor() as usize).min(rows.len());
|
||||
let mut y = list.min.y - (scroll - first as f32 * ROW_H);
|
||||
let project = app.session.project.clone();
|
||||
|
|
@ -635,6 +606,15 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|||
}
|
||||
});
|
||||
}
|
||||
// The scroll bars, over the rows (which would take their presses otherwise).
|
||||
if overflow > 0.0 {
|
||||
let track = Rect::from_min_max(pos2(opt_x0, list.max.y - 5.0), pos2(rect.max.x - 4.0, list.max.y - 1.0));
|
||||
app.ui.project_hscroll = widgets::scroll_bar(ui, &mut app.auto, "project.hscroll", track, hscroll, overflow, &t);
|
||||
}
|
||||
if max_scroll > 0.0 {
|
||||
let track = Rect::from_min_max(pos2(rect.max.x - 6.0, list.min.y + 1.0), pos2(rect.max.x - 2.0, list.max.y - 7.0));
|
||||
app.ui.project_scroll = widgets::scroll_bar(ui, &mut app.auto, "project.vscroll", track, scroll, max_scroll, &t);
|
||||
}
|
||||
// Dropping a dragged item on the list: into the folder under the pointer (or the folder of
|
||||
// the item under it), or the project root below the rows.
|
||||
if let Some(iid) = dragging
|
||||
|
|
|
|||
|
|
@ -66,14 +66,8 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|||
let ectx = EvalCtx { project: &project, comp_id: cid, comp: &comp, time: now, expr: expr.as_deref(), footage: None };
|
||||
let x0 = rect.min.x + PAD;
|
||||
let w = rect.width() - 2.0 * PAD;
|
||||
// Vertical scrolling: content height is known after drawing, so clamp with last frame's.
|
||||
let scroll_id = egui::Id::new("props-scroll");
|
||||
let (mut scroll, content_h): (f32, f32) = ui.data(|d| d.get_temp(scroll_id)).unwrap_or((0.0, 0.0));
|
||||
if ui.rect_contains_pointer(rect) {
|
||||
scroll -= ui.input(|i| i.smooth_scroll_delta.y);
|
||||
}
|
||||
scroll = scroll.clamp(0.0, (content_h - rect.height()).max(0.0));
|
||||
let top = rect.min.y + 8.0 - scroll;
|
||||
let scroll = widgets::PanelScroll::begin(ui, egui::Id::new("props-scroll"), rect);
|
||||
let top = rect.min.y + 8.0 - scroll.offset;
|
||||
let mut y = top;
|
||||
let mut actions: Actions = vec![];
|
||||
|
||||
|
|
@ -153,15 +147,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|||
}
|
||||
|
||||
y += 40.0;
|
||||
let content_h = y - top;
|
||||
ui.data_mut(|d| d.insert_temp(scroll_id, (scroll, content_h)));
|
||||
if content_h > rect.height() {
|
||||
// Thin scroll indicator.
|
||||
let frac = rect.height() / content_h;
|
||||
let bar_h = (rect.height() * frac).max(24.0);
|
||||
let by = rect.min.y + (rect.height() - bar_h) * (scroll / (content_h - rect.height()).max(1.0));
|
||||
p.rect_filled(Rect::from_min_size(pos2(rect.max.x - 5.0, by), vec2(3.0, bar_h)), 1.5, t.text_faint.gamma_multiply(0.6));
|
||||
}
|
||||
scroll.end(ui, &mut app.auto, "properties.scroll", y - top, &t);
|
||||
for (id, params) in actions {
|
||||
let params = match &text_target {
|
||||
Some(tt) if id == "layer.setText" => tt.params(params),
|
||||
|
|
|
|||
|
|
@ -527,7 +527,11 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|||
// ---------------------------------------------------------- items
|
||||
let list = Rect::from_min_max(pos2(rect.min.x, head.max.y), pos2(rect.max.x, rect.max.y - FOOT_H));
|
||||
let lp = p.with_clip_rect(list);
|
||||
let mut y = list.min.y;
|
||||
let scroll = widgets::PanelScroll::begin(ui, egui::Id::new("rq-scroll"), list);
|
||||
// Items scroll under the header and footer: clip their widgets (and hit tests) to the list.
|
||||
let panel_clip = ui.clip_rect();
|
||||
ui.set_clip_rect(list.intersect(panel_clip));
|
||||
let mut y = list.min.y - scroll.offset;
|
||||
if queue.is_empty() {
|
||||
lp.text(pos2(list.center().x, list.min.y + 30.0), Align2::CENTER_CENTER, "The render queue is empty.", Tokens::ui(12.0), t.text_faint);
|
||||
}
|
||||
|
|
@ -746,6 +750,9 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|||
}
|
||||
}
|
||||
}
|
||||
ui.set_clip_rect(panel_clip);
|
||||
let content = y + scroll.offset - list.min.y;
|
||||
scroll.end(ui, &mut app.auto, "renderQueue.scroll", content, &t);
|
||||
ui.data_mut(|d| match &editing {
|
||||
Some(e) => {
|
||||
d.insert_temp(editing_id, e.clone());
|
||||
|
|
|
|||
|
|
@ -86,7 +86,9 @@ pub fn character(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|||
let target = text_target(app);
|
||||
let enabled = target.is_some();
|
||||
let doc = target.as_ref().map(|t| t.doc.clone()).unwrap_or_default();
|
||||
let mut y = rect.min.y + 10.0;
|
||||
let scroll = widgets::PanelScroll::begin(ui, egui::Id::new("character-scroll"), rect);
|
||||
let top = rect.min.y + 10.0 - scroll.offset;
|
||||
let mut y = top;
|
||||
let x0 = rect.min.x + 10.0;
|
||||
let w = rect.width() - 20.0;
|
||||
let mut actions: Vec<serde_json::Value> = vec![];
|
||||
|
|
@ -279,7 +281,10 @@ pub fn character(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|||
actions.push(json!({"variations": {tag.clone(): nv}, "merge": format!("char-axis-{tag}")}));
|
||||
}
|
||||
}
|
||||
y += 20.0 + axes.len().div_ceil(2) as f32 * 24.0;
|
||||
}
|
||||
// (Room for the status line at the bottom.)
|
||||
scroll.end(ui, &mut app.auto, "character.scroll", y + 44.0 - top, &t);
|
||||
match &target {
|
||||
None => {
|
||||
p.text(pos2(rect.center().x, rect.max.y - 20.0), Align2::CENTER_CENTER, "Select a text layer", Tokens::ui(11.0), t.text_faint);
|
||||
|
|
@ -441,7 +446,9 @@ pub fn paragraph(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|||
let doc = target.as_ref().map(|t| t.doc.clone()).unwrap_or_default();
|
||||
let x0 = rect.min.x + 10.0;
|
||||
let w = rect.width() - 20.0;
|
||||
let mut y = rect.min.y + 10.0;
|
||||
let scroll = widgets::PanelScroll::begin(ui, egui::Id::new("paragraph-scroll"), rect);
|
||||
let top = rect.min.y + 10.0 - scroll.offset;
|
||||
let mut y = top;
|
||||
let mut actions: Vec<serde_json::Value> = vec![];
|
||||
let bw = ((w - 6.0 * 3.0) / 7.0).clamp(20.0, 28.0);
|
||||
for (i, (j, key)) in ALIGNS.into_iter().enumerate() {
|
||||
|
|
@ -530,6 +537,8 @@ pub fn paragraph(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|||
}
|
||||
app.auto.add("paragraph.hangingPunctuation", hr, "Roman Hanging Punctuation");
|
||||
p.text(pos2(x0 + 20.0, y + 7.0), Align2::LEFT_CENTER, "Roman Hanging Punctuation", Tokens::ui(11.5), t.text_dim);
|
||||
// (Room for the status line at the bottom.)
|
||||
scroll.end(ui, &mut app.auto, "paragraph.scroll", y + 44.0 - top, &t);
|
||||
if !enabled {
|
||||
p.text(pos2(rect.center().x, rect.max.y - 20.0), Align2::CENTER_CENTER, "Select a text layer", Tokens::ui(11.0), t.text_faint);
|
||||
}
|
||||
|
|
@ -544,51 +553,44 @@ pub fn paragraph(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|||
/// The font menu: recent fonts first, names in English or the fonts' own language, and a
|
||||
/// "Sample" preview in each font (Settings ▸ Type).
|
||||
fn font_popup(app: &EffectcraftApp, ui: &mut egui::Ui, id: egui::Id, pos: egui::Pos2, current: &str) -> Option<String> {
|
||||
if !ui.data(|d| d.get_temp::<bool>(id.with("open")).unwrap_or(false)) {
|
||||
if !widgets::popup_is_open(ui, id) {
|
||||
return None;
|
||||
}
|
||||
let rows = effectcraft_engine::font_menu(&app.session.prefs);
|
||||
let preview = app.session.prefs.type_.font_preview;
|
||||
let t = app.tokens;
|
||||
let mut chosen = None;
|
||||
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(if preview { 300.0 } else { 180.0 });
|
||||
egui::ScrollArea::vertical().max_height(420.0).show(ui, |ui| {
|
||||
for r in &rows {
|
||||
if r.family.is_empty() {
|
||||
ui.separator();
|
||||
continue;
|
||||
}
|
||||
let resp = ui.selectable_label(r.family == current, &r.display);
|
||||
if preview && ui.is_rect_visible(resp.rect) {
|
||||
let key = egui::Id::new(("font-preview", &r.family));
|
||||
let lines: std::sync::Arc<Vec<Vec<[f32; 2]>>> = match ui.data(|d| d.get_temp(key)) {
|
||||
Some(l) => l,
|
||||
None => {
|
||||
let l = std::sync::Arc::new(effectcraft_engine::font_preview(&r.family, 14.0));
|
||||
ui.data_mut(|d| d.insert_temp(key, l.clone()));
|
||||
l
|
||||
}
|
||||
};
|
||||
let o = pos2(resp.rect.max.x - 120.0, resp.rect.center().y + 5.0);
|
||||
for poly in lines.iter() {
|
||||
let pts: Vec<egui::Pos2> = poly.iter().map(|p| o + vec2(p[0], p[1])).collect();
|
||||
ui.painter().add(egui::Shape::line(pts, egui::Stroke::new(1.0, t.text_dim)));
|
||||
}
|
||||
}
|
||||
if resp.clicked() {
|
||||
chosen = Some(r.family.clone());
|
||||
let seps = rows.iter().filter(|r| r.family.is_empty()).count();
|
||||
widgets::popup_list(ui, id, pos, Rect::NOTHING, rows.len().saturating_sub(seps), seps, |ui| {
|
||||
ui.set_min_width(if preview { 300.0 } else { 180.0 });
|
||||
let mut chosen = None;
|
||||
for r in &rows {
|
||||
if r.family.is_empty() {
|
||||
ui.separator();
|
||||
continue;
|
||||
}
|
||||
let resp = ui.selectable_label(r.family == current, &r.display);
|
||||
if preview && ui.is_rect_visible(resp.rect) {
|
||||
let key = egui::Id::new(("font-preview", &r.family));
|
||||
let lines: std::sync::Arc<Vec<Vec<[f32; 2]>>> = match ui.data(|d| d.get_temp(key)) {
|
||||
Some(l) => l,
|
||||
None => {
|
||||
let l = std::sync::Arc::new(effectcraft_engine::font_preview(&r.family, 14.0));
|
||||
ui.data_mut(|d| d.insert_temp(key, l.clone()));
|
||||
l
|
||||
}
|
||||
};
|
||||
let o = pos2(resp.rect.max.x - 120.0, resp.rect.center().y + 5.0);
|
||||
for poly in lines.iter() {
|
||||
let pts: Vec<egui::Pos2> = poly.iter().map(|p| o + vec2(p[0], p[1])).collect();
|
||||
ui.painter().add(egui::Shape::line(pts, egui::Stroke::new(1.0, t.text_dim)));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
let outside = widgets::pressed_outside(ui.ctx(), &area.response);
|
||||
if chosen.is_some() || outside || ui.input(|i| i.key_pressed(egui::Key::Escape)) {
|
||||
ui.data_mut(|d| d.insert_temp(id.with("open"), false));
|
||||
}
|
||||
chosen
|
||||
}
|
||||
if resp.clicked() {
|
||||
chosen = Some(r.family.clone());
|
||||
}
|
||||
}
|
||||
chosen
|
||||
})
|
||||
}
|
||||
|
||||
/// Align panel: Align Layers to Selection / Composition, the six align buttons and the six
|
||||
|
|
|
|||
|
|
@ -1934,12 +1934,14 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|||
egui::Popup::menu(&lang).show(|ui| {
|
||||
for (cat, items) in effectcraft_engine::commands::expr_tools::language_menu() {
|
||||
ui.menu_button(cat, |ui| {
|
||||
for (label, text) in items {
|
||||
if ui.button(label).clicked() {
|
||||
picked = Some(text);
|
||||
ui.close();
|
||||
widgets::menu_scroll(ui, |ui| {
|
||||
for (label, text) in items {
|
||||
if ui.button(label).clicked() {
|
||||
picked = Some(text);
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -2438,18 +2440,12 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|||
if overflow > 0.0 {
|
||||
// Outline scroll bar along the top of the footer, under the columns.
|
||||
let track = Rect::from_min_max(pos2(rect.min.x + 2.0, footer.min.y + 1.0), pos2(graph_x0 - 3.0, footer.min.y + 5.0));
|
||||
let frac = left_w / natural_w;
|
||||
let tw = (track.width() * frac).max(20.0);
|
||||
let tx = track.min.x + (track.width() - tw) * (oscroll / overflow);
|
||||
let thumb = Rect::from_min_size(pos2(tx, track.min.y), vec2(tw, track.height()));
|
||||
p.rect_filled(track, 2.0, t.field_bg);
|
||||
let sresp = ui.interact(track.expand2(vec2(0.0, 2.0)), egui::Id::new("tl-outline-scroll"), Sense::drag());
|
||||
p.rect_filled(thumb, 2.0, if sresp.dragged() || sresp.hovered() { t.text_dim } else { t.text_faint });
|
||||
app.auto.add("timeline.outlineScroll", track, &format!("{oscroll}/{overflow}"));
|
||||
if sresp.dragged() {
|
||||
let k = overflow / (track.width() - tw).max(1.0);
|
||||
app.ui.timeline.outline_scroll = (oscroll + sresp.drag_delta().x * k).clamp(0.0, overflow);
|
||||
}
|
||||
app.ui.timeline.outline_scroll = widgets::scroll_bar(ui, &mut app.auto, "timeline.outlineScroll", track, oscroll, overflow, &t);
|
||||
}
|
||||
if max_scroll > 0.0 {
|
||||
// The layer rows' scroll bar, on the right edge beside the time graph.
|
||||
let track = Rect::from_min_max(pos2(rect.max.x - 6.0, rows_rect.min.y + 1.0), pos2(rect.max.x - 2.0, rows_rect.max.y - 1.0));
|
||||
app.ui.timeline.scroll_y = widgets::scroll_bar(ui, &mut app.auto, "timeline.scroll", track, app.ui.timeline.scroll_y, max_scroll, &t);
|
||||
}
|
||||
let tgl = Rect::from_min_size(pos2(footer.min.x + 10.0, footer.min.y + 3.0), vec2(150.0, 18.0));
|
||||
let tresp = ui.interact(tgl, egui::Id::new("tl-toggle-modes"), Sense::click());
|
||||
|
|
@ -2958,12 +2954,14 @@ pub(crate) fn layer_menu(ui: &mut egui::Ui, layers: &[u64], actions: &mut Vec<(S
|
|||
item(ui, "Freeze on Last Frame", "layer.freezeOnLastFrame", json!({"layers": layers}));
|
||||
});
|
||||
ui.menu_button("Blending Mode", |ui| {
|
||||
for m in BlendMode::ALL {
|
||||
item(ui, m.label(), "layer.setBlendMode", json!({"layers": layers, "mode": m.label()}));
|
||||
if m.ends_group() {
|
||||
ui.separator();
|
||||
widgets::menu_scroll(ui, |ui| {
|
||||
for m in BlendMode::ALL {
|
||||
item(ui, m.label(), "layer.setBlendMode", json!({"layers": layers, "mode": m.label()}));
|
||||
if m.ends_group() {
|
||||
ui.separator();
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
ui.menu_button("Arrange", |ui| {
|
||||
item(ui, "Bring Layer to Front", "layer.arrange", json!({"layers": layers, "to": "front"}));
|
||||
|
|
|
|||
|
|
@ -962,12 +962,14 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|||
let hits = ui.data(|d| d.get_temp::<Vec<(u64, String)>>(menu_hits_id)).unwrap_or_default();
|
||||
if !hits.is_empty() {
|
||||
ui.menu_button("Select", |ui| {
|
||||
for (id, name) in &hits {
|
||||
if ui.button(name).clicked() {
|
||||
context_actions.push(("layer.select".to_owned(), serde_json::json!({"layers":[id]})));
|
||||
ui.close();
|
||||
crate::widgets::menu_scroll(ui, |ui| {
|
||||
for (id, name) in &hits {
|
||||
if ui.button(name).clicked() {
|
||||
context_actions.push(("layer.select".to_owned(), serde_json::json!({"layers":[id]})));
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
ui.separator();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -455,34 +455,27 @@ pub(crate) fn draw_frame(app: &mut EffectcraftApp, ctx: &egui::Context, painter:
|
|||
/// `viewer.<auto>.<n>` automation ids.
|
||||
fn popup(app: &mut EffectcraftApp, ui: &mut egui::Ui, id: &str, anchor: Rect, items: &[(String, bool)], auto: &str) -> Option<usize> {
|
||||
let pid = egui::Id::new(id);
|
||||
let open: bool = ui.data(|d| d.get_temp(pid.with("open")).unwrap_or(false));
|
||||
if !open {
|
||||
if !widgets::popup_is_open(ui, pid) {
|
||||
return None;
|
||||
}
|
||||
let h: f32 = items.iter().map(|(l, _)| if l == "-" { 9.0 } else { 21.0 }).sum::<f32>() + 14.0;
|
||||
let pos = anchor.left_top() - vec2(0.0, h + 4.0);
|
||||
let mut chosen = None;
|
||||
let area = egui::Area::new(pid.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(170.0);
|
||||
for (i, (label, on)) in items.iter().enumerate() {
|
||||
if label == "-" {
|
||||
ui.separator();
|
||||
continue;
|
||||
}
|
||||
let r = ui.selectable_label(*on, label.as_str());
|
||||
app.auto.add(&format!("viewer.{auto}.{i}"), r.rect, label);
|
||||
if r.clicked() {
|
||||
chosen = Some(i);
|
||||
}
|
||||
let seps = items.iter().filter(|(l, _)| l == "-").count();
|
||||
widgets::popup_list(ui, pid, anchor.left_top() - vec2(0.0, h + 4.0), anchor, items.len().saturating_sub(seps), seps, |ui| {
|
||||
ui.set_min_width(170.0);
|
||||
let mut chosen = None;
|
||||
for (i, (label, on)) in items.iter().enumerate() {
|
||||
if label == "-" {
|
||||
ui.separator();
|
||||
continue;
|
||||
}
|
||||
});
|
||||
});
|
||||
let outside = widgets::pressed_outside(ui.ctx(), &area.response) && !anchor.contains(ui.input(|i| i.pointer.interact_pos()).unwrap_or_default());
|
||||
if chosen.is_some() || outside || ui.input(|i| i.key_pressed(egui::Key::Escape)) {
|
||||
ui.data_mut(|d| d.insert_temp(pid.with("open"), false));
|
||||
}
|
||||
chosen
|
||||
let r = ui.selectable_label(*on, label.as_str());
|
||||
app.auto.add(&format!("viewer.{auto}.{i}"), r.rect, label);
|
||||
if r.clicked() {
|
||||
chosen = Some(i);
|
||||
}
|
||||
}
|
||||
chosen
|
||||
})
|
||||
}
|
||||
|
||||
fn toggle_popup(ui: &egui::Ui, id: &str) {
|
||||
|
|
|
|||
|
|
@ -345,6 +345,9 @@ pub fn apply_visuals(ctx: &egui::Context, t: &Tokens) {
|
|||
s.spacing.button_padding = egui::vec2(8.0, 3.0);
|
||||
s.spacing.interact_size.y = 22.0;
|
||||
s.spacing.menu_margin = egui::Margin::same(5);
|
||||
// Scroll bars always show (egui's float in only over the list), so long menus and lists
|
||||
// can be dragged without a mouse wheel or touchpad (#269).
|
||||
s.spacing.scroll = egui::style::ScrollStyle::solid();
|
||||
s.text_styles.insert(TextStyle::Body, FontId::new(12.0, FontFamily::Proportional));
|
||||
s.text_styles.insert(TextStyle::Button, FontId::new(12.0, FontFamily::Proportional));
|
||||
s.text_styles.insert(TextStyle::Small, FontId::new(11.0, FontFamily::Proportional));
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
use egui::{Align2, Color32, Rect, Response, Sense, Stroke, StrokeKind, Ui, pos2, vec2};
|
||||
|
||||
use crate::automation::Registry;
|
||||
use crate::icons::{self, Icon};
|
||||
use crate::theme::Tokens;
|
||||
|
||||
|
|
@ -277,6 +278,79 @@ pub fn checkbox(ui: &mut Ui, rect: Rect, on: bool, t: &Tokens, id: egui::Id) ->
|
|||
resp
|
||||
}
|
||||
|
||||
/// A scroll bar along `track` (vertical when it is taller than wide) for content scrolled
|
||||
/// `scroll` of `max`, registered for automation as `key` ("scroll/max"): drag the thumb, or
|
||||
/// press the track beside it to centre the thumb there. The thumb's length is the share in view
|
||||
/// (the view is about as long as its track), at least 16 points but never longer than the track
|
||||
/// (a panel only a few points tall still draws, #231). Returns the new scroll.
|
||||
pub fn scroll_bar(ui: &mut Ui, auto: &mut Registry, key: &str, track: Rect, scroll: f32, max: f32, t: &Tokens) -> f32 {
|
||||
if !track.is_positive() {
|
||||
// (A panel too short for its bar.)
|
||||
return scroll.clamp(0.0, max.max(0.0));
|
||||
}
|
||||
let vertical = track.height() >= track.width();
|
||||
let a = usize::from(vertical);
|
||||
let len = track.size()[a];
|
||||
let thumb_len = (len * len / (len + max.max(0.0)).max(1.0)).max(16.0).min(len).max(0.0);
|
||||
let travel = (len - thumb_len).max(1.0);
|
||||
let thumb_at = |s: f32| track.min[a] + (len - thumb_len) * if max > 0.0 { (s / max).clamp(0.0, 1.0) } else { 0.0 };
|
||||
let resp = ui.interact(track.expand2(if vertical { vec2(3.0, 0.0) } else { vec2(0.0, 3.0) }), egui::Id::new(key), Sense::drag());
|
||||
let pressed = resp.is_pointer_button_down_on() && ui.input(|i| i.pointer.any_pressed());
|
||||
let mut s = scroll;
|
||||
if let Some(p) = resp.interact_pointer_pos().filter(|p| pressed && !(thumb_at(scroll)..=thumb_at(scroll) + thumb_len).contains(&p[a])) {
|
||||
s = (p[a] - thumb_len / 2.0 - track.min[a]) * max / travel;
|
||||
} else if resp.dragged() {
|
||||
s += resp.drag_delta()[a] * max / travel;
|
||||
}
|
||||
let s = s.clamp(0.0, max.max(0.0));
|
||||
let thumb = if vertical {
|
||||
Rect::from_min_size(pos2(track.min.x, thumb_at(s)), vec2(track.width(), thumb_len))
|
||||
} else {
|
||||
Rect::from_min_size(pos2(thumb_at(s), track.min.y), vec2(thumb_len, track.height()))
|
||||
};
|
||||
ui.painter().rect_filled(track, 2.0, t.field_bg);
|
||||
ui.painter().rect_filled(thumb, 2.0, if resp.hovered() || resp.dragged() { t.text_dim } else { t.text_faint });
|
||||
auto.add(key, track, &format!("{s}/{max}"));
|
||||
if s != scroll {
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
/// Vertical scrolling for a panel that lays out its own rows: [`PanelScroll::begin`] gives the
|
||||
/// offset to draw them at (moved by the mouse wheel over `area`), [`PanelScroll::end`] takes the
|
||||
/// rows' height and draws a [`scroll_bar`] on the right edge while they overflow, so every
|
||||
/// panel can be scrolled without a wheel or touchpad (#271).
|
||||
pub struct PanelScroll {
|
||||
id: egui::Id,
|
||||
area: Rect,
|
||||
/// How far the rows are scrolled up.
|
||||
pub offset: f32,
|
||||
}
|
||||
|
||||
impl PanelScroll {
|
||||
pub fn begin(ui: &Ui, id: egui::Id, area: Rect) -> Self {
|
||||
// (Clamped to last frame's overflow: the rows' height is known once they are drawn.)
|
||||
let (mut offset, max): (f32, f32) = ui.data(|d| d.get_temp(id)).unwrap_or_default();
|
||||
if ui.rect_contains_pointer(area) {
|
||||
offset -= ui.input(|i| i.smooth_scroll_delta.y);
|
||||
}
|
||||
Self { id, area, offset: offset.clamp(0.0, max.max(0.0)) }
|
||||
}
|
||||
|
||||
/// The rows were `content` points tall: keep the offset, with the scroll bar registered for
|
||||
/// automation as `key`.
|
||||
pub fn end(self, ui: &mut Ui, auto: &mut Registry, key: &str, content: f32, t: &Tokens) {
|
||||
let max = (content - self.area.height()).max(0.0);
|
||||
let mut offset = self.offset.min(max);
|
||||
if max > 0.0 {
|
||||
let track = Rect::from_min_max(pos2(self.area.max.x - 6.0, self.area.min.y + 1.0), pos2(self.area.max.x - 2.0, self.area.max.y - 1.0));
|
||||
offset = scroll_bar(ui, auto, key, track, offset, max, t);
|
||||
}
|
||||
ui.data_mut(|d| d.insert_temp(self.id, (offset, max)));
|
||||
}
|
||||
}
|
||||
|
||||
/// A pointer press this frame landed outside a popup `Area` (its `show` response). egui
|
||||
/// hit-tests a fixed-position area where it was asked to go, not where it was moved to fit the
|
||||
/// window, so `contains_pointer` / `hovered` are false over a menu shifted up from the bottom of
|
||||
|
|
@ -293,46 +367,60 @@ pub fn check_label(on: bool, label: &str) -> String {
|
|||
|
||||
/// Show a popup menu anchored at `pos` with string options; returns the chosen index.
|
||||
pub fn popup_menu(ui: &mut Ui, id: egui::Id, pos: egui::Pos2, options: &[String], current: Option<usize>) -> Option<usize> {
|
||||
let mut chosen = None;
|
||||
let open_id = id.with("open");
|
||||
let open: bool = ui.data(|d| d.get_temp(open_id).unwrap_or(false));
|
||||
if !open {
|
||||
if !popup_is_open(ui, id) {
|
||||
return None;
|
||||
}
|
||||
// As tall as the window allows (a shape layer's Add menu has 20 entries), scrolling beyond,
|
||||
// and moved up when it would run past the bottom of the window (the Timeline's menus).
|
||||
let seps = options.iter().filter(|o| *o == "-").count();
|
||||
popup_list(ui, id, pos, Rect::NOTHING, options.len().saturating_sub(seps), seps, |ui| {
|
||||
ui.set_min_width(160.0);
|
||||
let mut chosen = None;
|
||||
for (i, o) in options.iter().enumerate() {
|
||||
if o == "-" {
|
||||
ui.separator();
|
||||
} else if ui.selectable_label(current == Some(i), o).clicked() {
|
||||
chosen = Some(i);
|
||||
}
|
||||
}
|
||||
chosen
|
||||
})
|
||||
}
|
||||
|
||||
/// The open popup list `id` at `pos`, drawn by `body` (`rows` entries and `seps` separators,
|
||||
/// for its height): as tall as the window allows, scrolling beyond with a scroll bar (a shape
|
||||
/// layer's Add menu has 20 entries, the blend modes 40, the fonts hundreds), and moved up when
|
||||
/// it would run past the bottom of the window (the Timeline's menus, #269). Closes when `body`
|
||||
/// returns a choice, on Escape, or on a press outside it and outside `anchor` (a control that
|
||||
/// toggles the list itself). Returns the choice.
|
||||
pub fn popup_list<R>(ui: &mut Ui, id: egui::Id, pos: egui::Pos2, anchor: Rect, rows: usize, seps: usize, body: impl FnOnce(&mut Ui) -> Option<R>) -> Option<R> {
|
||||
let screen = ui.ctx().content_rect();
|
||||
let max_h = (screen.height() - 24.0).max(120.0);
|
||||
let sp = ui.spacing();
|
||||
let seps = options.iter().filter(|o| *o == "-").count() as f32;
|
||||
let rows = options.len() as f32 - seps;
|
||||
let est = (rows * (sp.interact_size.y + sp.item_spacing.y + 4.0) + seps * (2.0 * sp.item_spacing.y + 2.0) + 16.0).min(max_h + 12.0);
|
||||
let est = (rows as f32 * (sp.interact_size.y + sp.item_spacing.y + 4.0) + seps as f32 * (2.0 * sp.item_spacing.y + 2.0) + 16.0).min(max_h + 12.0);
|
||||
let pos = egui::pos2(pos.x, pos.y.min(screen.bottom() - est).max(screen.top()));
|
||||
let mut chosen = None;
|
||||
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(160.0);
|
||||
// (An area's content is laid out in last frame's size: ask for the estimate.)
|
||||
egui::ScrollArea::vertical().max_height(max_h).min_scrolled_height((est - 16.0).min(max_h)).show(ui, |ui| {
|
||||
for (i, o) in options.iter().enumerate() {
|
||||
if o == "-" {
|
||||
ui.separator();
|
||||
continue;
|
||||
}
|
||||
let sel = current == Some(i);
|
||||
if ui.selectable_label(sel, o).clicked() {
|
||||
chosen = Some(i);
|
||||
}
|
||||
}
|
||||
});
|
||||
egui::ScrollArea::vertical().max_height(max_h).min_scrolled_height((est - 16.0).min(max_h)).show(ui, |ui| chosen = body(ui));
|
||||
});
|
||||
});
|
||||
let clicked_outside = pressed_outside(ui.ctx(), &area.response);
|
||||
if chosen.is_some() || clicked_outside || ui.input(|i| i.key_pressed(egui::Key::Escape)) {
|
||||
ui.data_mut(|d| d.insert_temp(open_id, false));
|
||||
let outside = pressed_outside(ui.ctx(), &area.response) && !ui.input(|i| i.pointer.interact_pos()).is_some_and(|p| anchor.contains(p));
|
||||
if chosen.is_some() || outside || ui.input(|i| i.key_pressed(egui::Key::Escape)) {
|
||||
ui.data_mut(|d| d.insert_temp(id.with("open"), false));
|
||||
}
|
||||
chosen
|
||||
}
|
||||
|
||||
/// A menu's (or submenu's) entries, scrolling with a scroll bar when they are taller than the
|
||||
/// window instead of running off it (Edit on a short screen, #269; Blending Mode). Others show
|
||||
/// whole: egui sizes a new menu from a default 400 pt area, so without a minimum a longer one
|
||||
/// (Window ▸ Workspace) got stuck at that height with its last entries scrolled out of view
|
||||
/// (#191).
|
||||
pub fn menu_scroll<R>(ui: &mut Ui, add: impl FnOnce(&mut Ui) -> R) -> R {
|
||||
let max_h = ui.ctx().content_rect().height() - 40.0;
|
||||
egui::ScrollArea::vertical().max_height(max_h).min_scrolled_height(max_h).show(ui, add).inner
|
||||
}
|
||||
|
||||
/// Whether a [`popup_menu`] is open (build its options only then: they can be long).
|
||||
pub fn popup_is_open(ui: &Ui, id: egui::Id) -> bool {
|
||||
ui.data(|d| d.get_temp(id.with("open")).unwrap_or(false))
|
||||
|
|
|
|||
|
|
@ -153,6 +153,20 @@ fn saved_workspace_is_listed_in_the_workspace_menu() {
|
|||
assert_eq!(h.state().ui.dock, saved, "its layout came back");
|
||||
}
|
||||
|
||||
/// Learn opens the Home screen's tutorials; clicking another workspace tab closes them again,
|
||||
/// however quickly the tabs are clicked (they stayed over every workspace, #272).
|
||||
#[test]
|
||||
fn leaving_the_learn_workspace_closes_its_tutorials() {
|
||||
let mut h = harness();
|
||||
for name in ["Learn", "Default", "Learn", "Review", "Learn", "Learn", "Small Screen", "Standard", "Learn", "Default"] {
|
||||
click(&mut h, &format!("header.workspace.{name}"));
|
||||
assert_eq!(h.state().ui.workspace, name);
|
||||
let learn = name == "Learn";
|
||||
assert_eq!(h.state().ui.start_screen, learn, "{name}: the Home screen shows only for Learn");
|
||||
assert_eq!(!h.state().auto.query("home.learn.").is_empty(), learn, "{name}: the tutorials show only for Learn");
|
||||
}
|
||||
}
|
||||
|
||||
/// Headless look at the panels (wgpu offscreen; needs a GPU adapter). Run with
|
||||
/// `PANELS_SNAPSHOT=/abs/dir cargo test -p effectcraft-ui-egui --test ui_panels -- --ignored`.
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,145 @@
|
|||
//! Long menus and lists scroll instead of running off the window, with a scroll bar to drag
|
||||
//! (egui_kittest, UI logic only).
|
||||
|
||||
use effectcraft_engine::Session;
|
||||
use effectcraft_ui_egui::EffectcraftApp;
|
||||
use egui::{Event, Pos2, Rect, pos2, vec2};
|
||||
use egui_kittest::Harness;
|
||||
use egui_kittest::kittest::Queryable;
|
||||
use serde_json::json;
|
||||
|
||||
fn harness(w: f32, h: f32) -> Harness<'static, EffectcraftApp> {
|
||||
let mut s = Session::default();
|
||||
s.execute("file.openDemoProject", json!({})).unwrap();
|
||||
let mut h = Harness::builder().with_size(vec2(w, h)).build_eframe(|_| EffectcraftApp::new(s));
|
||||
h.run_steps(3);
|
||||
h
|
||||
}
|
||||
|
||||
fn rect(h: &Harness<'_, EffectcraftApp>, id: &str) -> Rect {
|
||||
let e = h.state().auto.find(id).unwrap_or_else(|| panic!("no {id}"));
|
||||
Rect::from_min_size(pos2(e.rect[0], e.rect[1]), vec2(e.rect[2], e.rect[3]))
|
||||
}
|
||||
|
||||
fn click_at(h: &mut Harness<'_, EffectcraftApp>, p: Pos2) {
|
||||
h.input_mut().events.push(Event::PointerMoved(p));
|
||||
h.input_mut().events.push(Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed: true, modifiers: Default::default() });
|
||||
h.step();
|
||||
h.input_mut().events.push(Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed: false, modifiers: Default::default() });
|
||||
h.run_steps(2);
|
||||
}
|
||||
|
||||
fn wheel(h: &mut Harness<'_, EffectcraftApp>, at: Pos2, dy: f32) {
|
||||
h.input_mut().events.push(Event::PointerMoved(at));
|
||||
h.input_mut().events.push(Event::MouseWheel {
|
||||
unit: egui::MouseWheelUnit::Point,
|
||||
delta: vec2(0.0, dy),
|
||||
modifiers: Default::default(),
|
||||
phase: egui::TouchPhase::Move,
|
||||
});
|
||||
h.run_steps(20);
|
||||
}
|
||||
|
||||
/// Edit is taller than a short window: it scrolls (with the wheel, and a scroll bar that always
|
||||
/// shows) so its last entries can be reached and clicked, instead of running off the bottom of
|
||||
/// the window (#269).
|
||||
#[test]
|
||||
fn a_menu_taller_than_the_window_scrolls() {
|
||||
let mut h = harness(1200.0, 520.0);
|
||||
assert!(!h.ctx.global_style().spacing.scroll.floating, "scroll bars show without hovering the list");
|
||||
let edit = rect(&h, "menu.Edit");
|
||||
click_at(&mut h, edit.center());
|
||||
let last = h.query_by_label_contains("Keyboard Shortcuts").expect("the Edit menu is open").rect();
|
||||
assert!(last.max.y > 520.0, "Edit is taller than the window ({last:?})");
|
||||
let first = h.query_by_label_contains("Quick Apply").expect("Quick Apply").rect();
|
||||
wheel(&mut h, first.center(), -2000.0);
|
||||
let last = h.query_by_label_contains("Keyboard Shortcuts").expect("still open").rect();
|
||||
assert!(last.min.y >= 0.0 && last.max.y <= 520.0, "scrolled into the window ({last:?})");
|
||||
click_at(&mut h, last.center());
|
||||
assert_eq!(h.state().dialog, Some(effectcraft_ui_egui::Dialog::Shortcuts), "the entry takes the click");
|
||||
}
|
||||
|
||||
fn drag(h: &mut Harness<'_, EffectcraftApp>, from: Pos2, to: Pos2) {
|
||||
h.input_mut().events.push(Event::PointerMoved(from));
|
||||
h.input_mut().events.push(Event::PointerButton { pos: from, button: egui::PointerButton::Primary, pressed: true, modifiers: Default::default() });
|
||||
h.step();
|
||||
for i in 1..=8 {
|
||||
h.input_mut().events.push(Event::PointerMoved(from + (to - from) * (i as f32 / 8.0)));
|
||||
h.step();
|
||||
}
|
||||
h.input_mut().events.push(Event::PointerButton { pos: to, button: egui::PointerButton::Primary, pressed: false, modifiers: Default::default() });
|
||||
h.run_steps(2);
|
||||
}
|
||||
|
||||
/// A scroll bar's "scroll/max" automation label.
|
||||
fn scrolled(h: &Harness<'_, EffectcraftApp>, id: &str) -> (f32, f32) {
|
||||
let label = h.state().auto.find(id).unwrap_or_else(|| panic!("no {id}")).label.clone();
|
||||
let (s, max) = label.split_once('/').unwrap_or_else(|| panic!("{id}: {label}"));
|
||||
(s.parse().unwrap(), max.parse().unwrap())
|
||||
}
|
||||
|
||||
/// Every panel that scrolls its own rows shows a scroll bar while they overflow, and dragging it
|
||||
/// scrolls them: without a mouse wheel or touchpad there was no way down most panels (#271).
|
||||
#[test]
|
||||
fn panels_taller_than_their_space_have_a_scroll_bar_to_drag() {
|
||||
use effectcraft_ui_egui::dock::PanelKind;
|
||||
// (The Project panel's list starts below its item preview and search: a taller window.)
|
||||
for (panel, id, height) in [
|
||||
(PanelKind::EffectControls, "effectControls.scroll", 240.0),
|
||||
(PanelKind::Properties, "properties.scroll", 240.0),
|
||||
(PanelKind::EffectsPresets, "effects.scroll", 240.0),
|
||||
(PanelKind::Project, "project.vscroll", 330.0),
|
||||
(PanelKind::Timeline, "timeline.scroll", 240.0),
|
||||
(PanelKind::RenderQueue, "renderQueue.scroll", 240.0),
|
||||
(PanelKind::History, "history.scroll", 240.0),
|
||||
(PanelKind::Preview, "preview.scroll", 240.0),
|
||||
(PanelKind::Character, "character.scroll", 240.0),
|
||||
(PanelKind::Paragraph, "paragraph.scroll", 240.0),
|
||||
(PanelKind::Info, "info.scroll", 240.0),
|
||||
] {
|
||||
let mut h = harness(1200.0, height);
|
||||
let app = h.state_mut();
|
||||
let lid = app.session.active_comp().unwrap().layers[0].id.0;
|
||||
app.session.execute("layer.select", json!({"layers": [lid]})).unwrap();
|
||||
for e in ["slider", "angle", "checkbox", "color", "point"] {
|
||||
app.session.execute("effect.apply", json!({"effect": format!("ec.control.{e}"), "layers": [lid]})).unwrap();
|
||||
}
|
||||
for _ in 0..6 {
|
||||
app.session.execute("renderQueue.add", json!({})).unwrap();
|
||||
}
|
||||
app.show_panel(panel);
|
||||
app.ui.maximized = Some(panel);
|
||||
h.run_steps(3);
|
||||
let (_, max) = scrolled(&h, id);
|
||||
assert!(max > 0.0, "{id}: the rows overflow");
|
||||
let bar = rect(&h, id);
|
||||
// A press on the track moves the thumb there (History starts at its current state, the
|
||||
// last); then the thumb drags to the end.
|
||||
click_at(&mut h, pos2(bar.center().x, bar.min.y + 2.0));
|
||||
assert_eq!(scrolled(&h, id).0, 0.0, "{id}: at the top");
|
||||
drag(&mut h, pos2(bar.center().x, bar.min.y + 6.0), pos2(bar.center().x, bar.max.y + 40.0));
|
||||
assert_eq!(scrolled(&h, id).0, max, "{id}: dragged to the end");
|
||||
}
|
||||
}
|
||||
|
||||
/// In a narrow Effect Controls panel an animated point's values stay clear of its keyframe
|
||||
/// navigator (â—€ â—† â–¶), which was drawn over them (#271).
|
||||
#[test]
|
||||
fn effect_values_stay_clear_of_the_keyframe_navigator() {
|
||||
use effectcraft_ui_egui::dock::PanelKind;
|
||||
let mut h = harness(1600.0, 1000.0);
|
||||
let app = h.state_mut();
|
||||
let lid = app.session.active_comp().unwrap().layers[0].id.0;
|
||||
app.session.execute("layer.select", json!({"layers": [lid]})).unwrap();
|
||||
app.session.execute("effect.apply", json!({"effect": "ec.control.point", "layers": [lid]})).unwrap();
|
||||
let path = "effects/#1/point";
|
||||
app.session.execute("prop.toggleAnimation", json!({"layer": lid, "path": path})).unwrap();
|
||||
app.session.execute("prop.set", json!({"layer": lid, "path": path, "value": [123456.7, 123456.7]})).unwrap();
|
||||
let uid = app.session.active_comp().unwrap().layer(effectcraft_engine::project::LayerId(lid)).unwrap().props.prop(path).unwrap().uid;
|
||||
assert!(app.edit_layout(|l| l.float(PanelKind::EffectControls, [300.0, 150.0, 330.0, 500.0])));
|
||||
h.run_steps(3);
|
||||
let value = rect(&h, &format!("effectControls.prop.{uid}.value.1"));
|
||||
let nav = rect(&h, &format!("effectControls.prop.{uid}.prevKey"));
|
||||
assert!(value.max.x <= nav.min.x, "the value ({value:?}) ends before the navigator ({nav:?})");
|
||||
assert!(value.min.x > rect(&h, &format!("effectControls.prop.{uid}.stopwatch")).max.x, "and after the stopwatch");
|
||||
}
|
||||
Loading…
Reference in New Issue