Merge pull request #1416 from vangel76/fix-1322-properties-scroll
#1322: the Properties panel scrolls
This commit is contained in:
commit
f5b8f11271
2 files changed
+174
-94
No files matched your search
@@ -849,116 +849,130 @@ pub fn properties_panel(app: &mut FilmcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
}
|
||||
open
|
||||
};
|
||||
let eff_idx = |id: &str| it.effects.iter().position(|e| e.effect == id);
|
||||
let val = |eid: &str, p: &str| it.effect(eid).and_then(|e| e.param(p)).map(|p| p.value_at(mt));
|
||||
if kind == TrackKind::Video {
|
||||
if section(&mut bui, app, "Transform", eff_idx("motion").map(|i| (i, clip.0)), &mut actions) {
|
||||
for (label, p, unit, speed, range) in [
|
||||
(tl!("Position"), "position", "", 1.0, (-100_000.0, 100_000.0)),
|
||||
(tl!("Anchor point"), "anchor", "", 1.0, (-100_000.0, 100_000.0)),
|
||||
(tl!("Scale"), "scale", " %", 0.5, (0.0, 10000.0)),
|
||||
(tl!("Rotation"), "rotation", " °", 0.5, (-36000.0, 36000.0)),
|
||||
] {
|
||||
let r = row(&mut bui, label);
|
||||
// the sections scroll: a short panel would otherwise cut Audio and the speed button off (#1322)
|
||||
egui::ScrollArea::vertical().id_salt("props-scroll").auto_shrink([false, false]).show(&mut bui, |bui| {
|
||||
// keep the rows (and their keyframe arrows) clear of the scrollbar at the panel edge
|
||||
bui.set_max_width((bui.available_width() - 14.0).max(1.0));
|
||||
let eff_idx = |id: &str| it.effects.iter().position(|e| e.effect == id);
|
||||
let val = |eid: &str, p: &str| it.effect(eid).and_then(|e| e.param(p)).map(|p| p.value_at(mt));
|
||||
if kind == TrackKind::Video {
|
||||
if section(bui, app, "Transform", eff_idx("motion").map(|i| (i, clip.0)), &mut actions) {
|
||||
for (label, p, unit, speed, range) in [
|
||||
(tl!("Position"), "position", "", 1.0, (-100_000.0, 100_000.0)),
|
||||
(tl!("Anchor point"), "anchor", "", 1.0, (-100_000.0, 100_000.0)),
|
||||
(tl!("Scale"), "scale", " %", 0.5, (0.0, 10000.0)),
|
||||
(tl!("Rotation"), "rotation", " °", 0.5, (-36000.0, 36000.0)),
|
||||
] {
|
||||
let r = row(bui, label);
|
||||
let mut vui = bui.new_child(
|
||||
egui::UiBuilder::new()
|
||||
.max_rect(Rect::from_min_max(pos2(r.min.x + 150.0, r.min.y + 4.0), pos2(r.max.x - PROPS_VALUE_R, r.max.y - 4.0)))
|
||||
.layout(egui::Layout::left_to_right(egui::Align::Center)),
|
||||
);
|
||||
match val("motion", p) {
|
||||
Some(ParamValue::Vec2(v)) => {
|
||||
let (_, nx) = crate::widgets::hot_number(&mut vui, egui::Id::new(("pp", p, "x", clip.0)), v.x, speed, range, 0, " X", &t);
|
||||
vui.add_space(10.0);
|
||||
let (_, ny) = crate::widgets::hot_number(&mut vui, egui::Id::new(("pp", p, "y", clip.0)), v.y, speed, range, 0, " Y", &t);
|
||||
if nx.is_some() || ny.is_some() {
|
||||
actions.push((
|
||||
"effects.setParam".into(),
|
||||
json!({"clip": clip.0, "effect": "motion", "param": p, "value": [nx.unwrap_or(v.x), ny.unwrap_or(v.y)]}),
|
||||
));
|
||||
}
|
||||
}
|
||||
Some(ParamValue::Float(v)) => {
|
||||
let (_, nv) = crate::widgets::hot_number(
|
||||
&mut vui,
|
||||
egui::Id::new(("pp", p, clip.0)),
|
||||
v,
|
||||
speed,
|
||||
range,
|
||||
if p == "scale" { 0 } else { 1 },
|
||||
unit,
|
||||
&t,
|
||||
);
|
||||
if let Some(nv) = nv {
|
||||
actions.push(("effects.setParam".into(), json!({"clip": clip.0, "effect": "motion", "param": p, "value": nv})));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
properties_nav(app, bui, r, clip, &it, mt, "motion", p, &mut actions);
|
||||
}
|
||||
let r = row(bui, tl!("Opacity"));
|
||||
let mut vui = bui.new_child(
|
||||
egui::UiBuilder::new()
|
||||
.max_rect(Rect::from_min_max(pos2(r.min.x + 150.0, r.min.y + 4.0), pos2(r.max.x - PROPS_VALUE_R, r.max.y - 4.0)))
|
||||
.layout(egui::Layout::left_to_right(egui::Align::Center)),
|
||||
);
|
||||
match val("motion", p) {
|
||||
Some(ParamValue::Vec2(v)) => {
|
||||
let (_, nx) = crate::widgets::hot_number(&mut vui, egui::Id::new(("pp", p, "x", clip.0)), v.x, speed, range, 0, " X", &t);
|
||||
vui.add_space(10.0);
|
||||
let (_, ny) = crate::widgets::hot_number(&mut vui, egui::Id::new(("pp", p, "y", clip.0)), v.y, speed, range, 0, " Y", &t);
|
||||
if nx.is_some() || ny.is_some() {
|
||||
actions.push((
|
||||
"effects.setParam".into(),
|
||||
json!({"clip": clip.0, "effect": "motion", "param": p, "value": [nx.unwrap_or(v.x), ny.unwrap_or(v.y)]}),
|
||||
));
|
||||
}
|
||||
if let Some(ParamValue::Float(v)) = val("opacity", "opacity") {
|
||||
let (_, nv) = crate::widgets::hot_number(&mut vui, egui::Id::new(("pp-op", clip.0)), v, 0.5, (0.0, 100.0), 0, " %", &t);
|
||||
if let Some(nv) = nv {
|
||||
actions.push(("effects.setParam".into(), json!({"clip": clip.0, "effect": "opacity", "param": "opacity", "value": nv})));
|
||||
}
|
||||
Some(ParamValue::Float(v)) => {
|
||||
let (_, nv) =
|
||||
crate::widgets::hot_number(&mut vui, egui::Id::new(("pp", p, clip.0)), v, speed, range, if p == "scale" { 0 } else { 1 }, unit, &t);
|
||||
if let Some(nv) = nv {
|
||||
actions.push(("effects.setParam".into(), json!({"clip": clip.0, "effect": "motion", "param": p, "value": nv})));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
properties_nav(app, &mut bui, r, clip, &it, mt, "motion", p, &mut actions);
|
||||
properties_nav(app, bui, r, clip, &it, mt, "opacity", "opacity", &mut actions);
|
||||
}
|
||||
let r = row(&mut bui, tl!("Opacity"));
|
||||
let crop = eff_idx("crop");
|
||||
if section(bui, app, "Crop", crop.map(|i| (i, clip.0)), &mut actions) {
|
||||
for (label, p) in [(tl!("Left"), "left"), (tl!("Top"), "top"), (tl!("Right"), "right"), (tl!("Bottom"), "bottom")] {
|
||||
let r = row(bui, label);
|
||||
let mut vui = bui.new_child(
|
||||
egui::UiBuilder::new()
|
||||
.max_rect(Rect::from_min_max(pos2(r.min.x + 150.0, r.min.y + 4.0), pos2(r.max.x - PROPS_VALUE_R, r.max.y - 4.0)))
|
||||
.layout(egui::Layout::left_to_right(egui::Align::Center)),
|
||||
);
|
||||
let v = val("crop", p).and_then(|v| v.as_f64()).unwrap_or(0.0);
|
||||
let (_, nv) = crate::widgets::hot_number(&mut vui, egui::Id::new(("pp-crop", p, clip.0)), v, 0.2, (0.0, 100.0), 1, " %", &t);
|
||||
if let Some(nv) = nv {
|
||||
if crop.is_none() {
|
||||
actions.push(("effects.apply".into(), json!({"clips": [clip.0], "effect": "crop"})));
|
||||
}
|
||||
actions.push(("effects.setParam".into(), json!({"clip": clip.0, "effect": "crop", "param": p, "value": nv})));
|
||||
}
|
||||
properties_nav(app, bui, r, clip, &it, mt, "crop", p, &mut actions);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some((clip, it, _)) = clips.iter().find(|c| c.2 == TrackKind::Audio)
|
||||
&& section(bui, app, "Audio", it.effects.iter().position(|e| e.effect == "volume").map(|i| (i, clip.0)), &mut actions)
|
||||
{
|
||||
let clip = *clip;
|
||||
let mt = it.source_time_at(ph.clamp(it.start, (it.end() - Tick(1)).max(it.start)));
|
||||
let val = |eid: &str, p: &str| it.effect(eid).and_then(|e| e.param(p)).map(|p| p.value_at(mt));
|
||||
let r = row(bui, tl!("Level"));
|
||||
let mut vui = bui.new_child(
|
||||
egui::UiBuilder::new()
|
||||
.max_rect(Rect::from_min_max(pos2(r.min.x + 150.0, r.min.y + 4.0), pos2(r.max.x - PROPS_VALUE_R, r.max.y - 4.0)))
|
||||
.layout(egui::Layout::left_to_right(egui::Align::Center)),
|
||||
);
|
||||
if let Some(ParamValue::Float(v)) = val("opacity", "opacity") {
|
||||
let (_, nv) = crate::widgets::hot_number(&mut vui, egui::Id::new(("pp-op", clip.0)), v, 0.5, (0.0, 100.0), 0, " %", &t);
|
||||
if let Some(nv) = nv {
|
||||
actions.push(("effects.setParam".into(), json!({"clip": clip.0, "effect": "opacity", "param": "opacity", "value": nv})));
|
||||
}
|
||||
let v = val("volume", "level").and_then(|v| v.as_f64()).unwrap_or(0.0);
|
||||
let (_, nv) = crate::widgets::hot_number(&mut vui, egui::Id::new(("pp-vol", clip.0)), v, 0.1, (-96.0, 15.0), 1, " dB", &t);
|
||||
if let Some(nv) = nv {
|
||||
actions.push(("effects.setParam".into(), json!({"clip": clip.0, "effect": "volume", "param": "level", "value": nv})));
|
||||
}
|
||||
properties_nav(app, &mut bui, r, clip, &it, mt, "opacity", "opacity", &mut actions);
|
||||
}
|
||||
let crop = eff_idx("crop");
|
||||
if section(&mut bui, app, "Crop", crop.map(|i| (i, clip.0)), &mut actions) {
|
||||
for (label, p) in [(tl!("Left"), "left"), (tl!("Top"), "top"), (tl!("Right"), "right"), (tl!("Bottom"), "bottom")] {
|
||||
let r = row(&mut bui, label);
|
||||
let mut vui = bui.new_child(
|
||||
egui::UiBuilder::new()
|
||||
.max_rect(Rect::from_min_max(pos2(r.min.x + 150.0, r.min.y + 4.0), pos2(r.max.x - PROPS_VALUE_R, r.max.y - 4.0)))
|
||||
.layout(egui::Layout::left_to_right(egui::Align::Center)),
|
||||
);
|
||||
let v = val("crop", p).and_then(|v| v.as_f64()).unwrap_or(0.0);
|
||||
let (_, nv) = crate::widgets::hot_number(&mut vui, egui::Id::new(("pp-crop", p, clip.0)), v, 0.2, (0.0, 100.0), 1, " %", &t);
|
||||
if let Some(nv) = nv {
|
||||
if crop.is_none() {
|
||||
actions.push(("effects.apply".into(), json!({"clips": [clip.0], "effect": "crop"})));
|
||||
}
|
||||
actions.push(("effects.setParam".into(), json!({"clip": clip.0, "effect": "crop", "param": p, "value": nv})));
|
||||
}
|
||||
properties_nav(app, &mut bui, r, clip, &it, mt, "crop", p, &mut actions);
|
||||
properties_nav(app, bui, r, clip, it, mt, "volume", "level", &mut actions);
|
||||
let r = row(bui, tl!("Pan"));
|
||||
let mut vui = bui.new_child(
|
||||
egui::UiBuilder::new()
|
||||
.max_rect(Rect::from_min_max(pos2(r.min.x + 150.0, r.min.y + 4.0), pos2(r.max.x - PROPS_VALUE_R, r.max.y - 4.0)))
|
||||
.layout(egui::Layout::left_to_right(egui::Align::Center)),
|
||||
);
|
||||
let v = val("panner", "balance").and_then(|v| v.as_f64()).unwrap_or(0.0);
|
||||
let (_, nv) = crate::widgets::hot_number(&mut vui, egui::Id::new(("pp-pan", clip.0)), v, 0.5, (-100.0, 100.0), 0, "", &t);
|
||||
if let Some(nv) = nv {
|
||||
actions.push(("effects.setParam".into(), json!({"clip": clip.0, "effect": "panner", "param": "balance", "value": nv})));
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some((clip, it, _)) = clips.iter().find(|c| c.2 == TrackKind::Audio)
|
||||
&& section(&mut bui, app, "Audio", it.effects.iter().position(|e| e.effect == "volume").map(|i| (i, clip.0)), &mut actions)
|
||||
{
|
||||
let clip = *clip;
|
||||
let mt = it.source_time_at(ph.clamp(it.start, (it.end() - Tick(1)).max(it.start)));
|
||||
let val = |eid: &str, p: &str| it.effect(eid).and_then(|e| e.param(p)).map(|p| p.value_at(mt));
|
||||
let r = row(&mut bui, tl!("Level"));
|
||||
let mut vui = bui.new_child(
|
||||
egui::UiBuilder::new()
|
||||
.max_rect(Rect::from_min_max(pos2(r.min.x + 150.0, r.min.y + 4.0), pos2(r.max.x - PROPS_VALUE_R, r.max.y - 4.0)))
|
||||
.layout(egui::Layout::left_to_right(egui::Align::Center)),
|
||||
);
|
||||
let v = val("volume", "level").and_then(|v| v.as_f64()).unwrap_or(0.0);
|
||||
let (_, nv) = crate::widgets::hot_number(&mut vui, egui::Id::new(("pp-vol", clip.0)), v, 0.1, (-96.0, 15.0), 1, " dB", &t);
|
||||
if let Some(nv) = nv {
|
||||
actions.push(("effects.setParam".into(), json!({"clip": clip.0, "effect": "volume", "param": "level", "value": nv})));
|
||||
}
|
||||
properties_nav(app, &mut bui, r, clip, it, mt, "volume", "level", &mut actions);
|
||||
let r = row(&mut bui, tl!("Pan"));
|
||||
let mut vui = bui.new_child(
|
||||
egui::UiBuilder::new()
|
||||
.max_rect(Rect::from_min_max(pos2(r.min.x + 150.0, r.min.y + 4.0), pos2(r.max.x - PROPS_VALUE_R, r.max.y - 4.0)))
|
||||
.layout(egui::Layout::left_to_right(egui::Align::Center)),
|
||||
);
|
||||
let v = val("panner", "balance").and_then(|v| v.as_f64()).unwrap_or(0.0);
|
||||
let (_, nv) = crate::widgets::hot_number(&mut vui, egui::Id::new(("pp-pan", clip.0)), v, 0.5, (-100.0, 100.0), 0, "", &t);
|
||||
if let Some(nv) = nv {
|
||||
actions.push(("effects.setParam".into(), json!({"clip": clip.0, "effect": "panner", "param": "balance", "value": nv})));
|
||||
}
|
||||
}
|
||||
// speed footer
|
||||
bui.add_space(12.0);
|
||||
let (br, bresp) = bui.allocate_exact_size(vec2(118.0, 26.0), Sense::click());
|
||||
bui.painter().rect_filled(br, 4.0, if bresp.hovered() { t.hover } else { t.panel_bg });
|
||||
bui.painter().rect_stroke(br, 4.0, Stroke::new(1.0, t.separator), egui::StrokeKind::Inside);
|
||||
bui.painter().text(br.center(), Align2::CENTER_CENTER, tlf!("Speed {n}%", n = format!("{:.0}", it.speed * 100.0)), Tokens::ui(12.0), t.text);
|
||||
// speed footer
|
||||
bui.add_space(12.0);
|
||||
let (br, bresp) = bui.allocate_exact_size(vec2(118.0, 26.0), Sense::click());
|
||||
bui.painter().rect_filled(br, 4.0, if bresp.hovered() { t.hover } else { t.panel_bg });
|
||||
bui.painter().rect_stroke(br, 4.0, Stroke::new(1.0, t.separator), egui::StrokeKind::Inside);
|
||||
bui.painter().text(br.center(), Align2::CENTER_CENTER, tlf!("Speed {n}%", n = format!("{:.0}", it.speed * 100.0)), Tokens::ui(12.0), t.text);
|
||||
app.auto.add("properties.speed", br, "Speed");
|
||||
});
|
||||
run(app, ui.ctx(), actions);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
//! The Properties panel scrolls (#1322): with a short panel the Audio section and the speed button
|
||||
//! sit below the fold, and the mouse wheel has to bring them into view.
|
||||
|
||||
use std::sync::mpsc::{Sender, channel};
|
||||
|
||||
use egui_kittest::Harness;
|
||||
use filmcraft_engine::Session;
|
||||
use filmcraft_ui_egui::FilmcraftApp;
|
||||
use filmcraft_ui_egui::control::ControlRequest;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
struct Driver {
|
||||
harness: Harness<'static, FilmcraftApp>,
|
||||
tx: Sender<ControlRequest>,
|
||||
}
|
||||
|
||||
impl Driver {
|
||||
fn frames(&mut self, n: usize) {
|
||||
for _ in 0..n {
|
||||
let ctx = self.harness.ctx.clone();
|
||||
let mut raw = std::mem::take(self.harness.input_mut());
|
||||
eframe::App::raw_input_hook(self.harness.state_mut(), &ctx, &mut raw);
|
||||
*self.harness.input_mut() = raw;
|
||||
self.harness.step();
|
||||
}
|
||||
}
|
||||
fn ok(&mut self, method: &str, params: Value) -> Value {
|
||||
let (req, reply) = ControlRequest::new(method, params.clone());
|
||||
self.tx.send(req).expect("control channel");
|
||||
for _ in 0..600 {
|
||||
self.frames(1);
|
||||
if let Ok(r) = reply.try_recv() {
|
||||
assert_eq!(r["ok"], true, "{method} {params}: {r}");
|
||||
return r["result"].clone();
|
||||
}
|
||||
}
|
||||
panic!("no reply to {method} {params}");
|
||||
}
|
||||
fn top(&mut self, id: &str) -> Option<f64> {
|
||||
self.frames(2);
|
||||
let v = self.ok("ui.elements", json!({"prefix": id}));
|
||||
let e = v.as_array()?.iter().find(|e| e["id"] == id)?;
|
||||
e["rect"].as_array()?.get(1)?.as_f64()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn properties_panel_scrolls_to_its_lower_sections() {
|
||||
let mut session = Session::default();
|
||||
session.execute("file.openDemoProject", json!({})).expect("demo project");
|
||||
let (tx, rx) = channel();
|
||||
let app = FilmcraftApp::new(session).with_control(rx);
|
||||
let harness = Harness::builder().with_size(egui::vec2(1600.0, 560.0)).with_max_steps(10_000).build_eframe(move |_cc| app);
|
||||
let mut d = Driver { harness, tx };
|
||||
d.frames(4);
|
||||
let seq = d.ok("engine.execute", json!({"command": "sequence.inspect", "params": {}}));
|
||||
let clip = seq["video"][0]["items"][0]["clip"].as_u64().expect("first video clip");
|
||||
d.ok("engine.execute", json!({"command": "timeline.select", "params": {"clips": [clip]}}));
|
||||
d.ok("ui.panel.show", json!({"panel": "Properties"}));
|
||||
d.frames(3);
|
||||
let before = d.top("properties.speed").expect("the speed button is laid out");
|
||||
d.ok("ui.scroll", json!({"id": "panel.Properties", "dy": -2000.0}));
|
||||
d.frames(3);
|
||||
let after = d.top("properties.speed").expect("the speed button is still laid out");
|
||||
assert!(after < before - 20.0, "the wheel scrolls the panel: {before} -> {after}");
|
||||
}
|
||||
Reference in new issue
Block a user