mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-27 19:47:12 -06:00
Panels: every scrolling panel has a scroll bar to drag (#271)
Most panels lay out their own rows and scrolled only with the mouse wheel: Effect Controls, Effects & Presets, the Media Browser, Ease Presets and the Timeline's layer rows had no scroll bar, Properties and Preview a thin indicator that couldn't be dragged, and Render Queue, History, Markers, Character, Paragraph and Info didn't scroll at all, so a panel shorter than its content cut it off. Without a wheel or a working touchpad there was no way down. The Project panel had the only real scroll bars, hand-rolled, and its vertical one was hit-tested under the rows, so it only dragged where no row was. - widgets::scroll_bar: the Project panel's bars generalised (horizontal or vertical, drag the thumb or press the track to move it there, automation id with "scroll/max"); the Project panel's two bars and the Timeline's column bar use it, drawn over the rows. - widgets::PanelScroll: the wheel offset, its clamping and the bar for a panel's own rows, used by Effect Controls, Properties, Effects & Presets, Media Browser, Ease Presets, Preview, Render Queue, History (which still follows the current state), Markers, Character, Paragraph and Info. The Timeline's layer rows get a bar on the right edge. - Effect Controls: an animated vector's values (a puppet pin's Position) ran under the keyframe navigator in a narrow panel. They now end before it, moving left over the name, which is cut short. Test: ui_scrolling panels_taller_than_their_space_have_a_scroll_bar_to_drag (eleven panels in a short window: a press on the track and a thumb drag scroll them), effect_values_stay_clear_of_the_keyframe_navigator. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
6d2ef5a830
commit
11f3736025
@@ -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 {
|
||||
@@ -382,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());
|
||||
@@ -410,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})) {
|
||||
@@ -422,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);
|
||||
@@ -436,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);
|
||||
}
|
||||
|
||||
@@ -2440,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());
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -58,3 +58,88 @@ fn a_menu_taller_than_the_window_scrolls() {
|
||||
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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user