mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-25 02:27:00 -06:00
timeline: pick layer-valued properties from a layer drop-down (#2093)
The Timeline drew a layer-valued property (an Environment light's Source, a
Displacement Map's layer parameter) as read-only text holding the raw layer
id, so a light's Source could not be chosen in the UI at all - only through
prop.set from the command line, scripting or MCP. Effect Controls already
had a working drop-down for exactly these values: extract it into one shared
widgets::layer_picker (the current entry, "None" first, then "N. name" for
every layer of the comp, the list built only while the popup is open) and
draw it from both panels.
In the Timeline the pick writes through the same set closure as every other
value arm - an undoable prop.set (layer id, or null for None) - and
registers timeline.prop.<uid>.value like its neighbours, so the row names
the layer instead of exposing its id. Effect Controls keeps its rect,
automation id and write; its closed dropdown's "None", previously a
hardcoded literal, now follows the interface language. The dead "Layer {n}"
id text leaves the i18n catalogs with it.
ui_layer_source_picker drives both panels headlessly (open, pick, undo,
dismiss, current-entry highlight, translated None, picking None to clear a
set value), and an ignored wgpu snapshot test keeps the visual record
reproducible.
This commit is contained in:
@@ -294,7 +294,6 @@ const JAPANESE: &[(&str, &str)] = &[
|
||||
("Latest auto-save: {}", "最新の自動保存:{}"),
|
||||
("Layer", "レイヤー"),
|
||||
("Layer controls", "レイヤーコントロール"),
|
||||
("Layer {}", "レイヤー{}"),
|
||||
("Layer:", "レイヤー:"),
|
||||
("Layer: {}", "レイヤー:{}"),
|
||||
("Layer Styles", "レイヤースタイル"),
|
||||
@@ -1029,7 +1028,6 @@ const SIMPLIFIED_CHINESE: &[(&str, &str)] = &[
|
||||
("Latest auto-save: {}", "最新的自动保存:{}"),
|
||||
("Layer", "图层"),
|
||||
("Layer controls", "图层控制"),
|
||||
("Layer {}", "图层 {}"),
|
||||
("Layer:", "图层:"),
|
||||
("Layer: {}", "图层:{}"),
|
||||
("Layer Styles", "图层样式"),
|
||||
@@ -1769,7 +1767,6 @@ const TRADITIONAL_CHINESE: &[(&str, &str)] = &[
|
||||
("Latest auto-save: {}", "最新的自動儲存:{}"),
|
||||
("Layer", "圖層"),
|
||||
("Layer controls", "圖層控制"),
|
||||
("Layer {}", "圖層 {}"),
|
||||
("Layer:", "圖層:"),
|
||||
("Layer: {}", "圖層:{}"),
|
||||
("Layer Styles", "圖層樣式"),
|
||||
@@ -2512,7 +2509,6 @@ const UKRAINIAN: &[(&str, &str)] = &[
|
||||
("Latest auto-save: {}", "Останнє автозбереження: {}"),
|
||||
("Layer", "Шар"),
|
||||
("Layer controls", "Керування шарами"),
|
||||
("Layer {}", "Шар {}"),
|
||||
("Layer:", "Шар:"),
|
||||
("Layer: {}", "Шар: {}"),
|
||||
("Layer Styles", "Стилі шару"),
|
||||
@@ -3070,7 +3066,6 @@ const PORTUGUESE: &[(&str, &str)] = &[
|
||||
("Latest auto-save: {}", "Último salvamento automático: {}"),
|
||||
("Layer", "Camada"),
|
||||
("Layer controls", "Controles da camada"),
|
||||
("Layer {}", "Camada {}"),
|
||||
("Layer:", "Camada:"),
|
||||
("Layer: {}", "Camada: {}"),
|
||||
("Layer Styles", "Estilos de camada"),
|
||||
@@ -3529,7 +3524,6 @@ const CZECH: &[(&str, &str)] = &[
|
||||
("Latest auto-save: {}", "Nejnovější automatická záloha: {}"),
|
||||
("Layer", "Vrstva"),
|
||||
("Layer controls", "Ovládací prvky vrstvy"),
|
||||
("Layer {}", "Vrstva {}"),
|
||||
("Layer:", "Vrstva:"),
|
||||
("Layer: {}", "Vrstva: {}"),
|
||||
("Layer Styles", "Styly vrstvy"),
|
||||
@@ -4092,7 +4086,6 @@ const SPANISH: &[(&str, &str)] = &[
|
||||
("Latest auto-save: {}", "Último guardado automático: {}"),
|
||||
("Layer", "Capa"),
|
||||
("Layer controls", "Controles de capa"),
|
||||
("Layer {}", "Capa {}"),
|
||||
("Layer:", "Capa:"),
|
||||
("Layer: {}", "Capa: {}"),
|
||||
("Layer Styles", "Estilos de capa"),
|
||||
|
||||
@@ -441,19 +441,10 @@ fn prop_row(
|
||||
let comp = app.session.active_comp_arc();
|
||||
if let Some(comp) = comp {
|
||||
let dr = Rect::from_min_size(pos2(vx, cy - 9.0), vec2(((r.max.x - vx) * 0.55).clamp(90.0, 170.0), 18.0));
|
||||
let idx_of = |id: u64| comp.layers.iter().position(|x| x.id.0 == id);
|
||||
let name = l.and_then(|id| idx_of(id).map(|i| format!("{}. {}", i + 1, comp.layers[i].name))).unwrap_or_else(|| "None".into());
|
||||
let pop = egui::Id::new(("ec-lpop", uid));
|
||||
if widgets::dropdown(ui, dr, &name, &t, egui::Id::new(("ec-l", uid))).clicked() {
|
||||
widgets::open_popup(ui, pop);
|
||||
if let Some(chosen) = widgets::layer_picker(ui, dr, &comp.layers, *l, &t, egui::Id::new(("ec-l", uid))) {
|
||||
set(actions, chosen.map(|id| json!(id)).unwrap_or(serde_json::Value::Null));
|
||||
}
|
||||
app.auto.add(&format!("effectControls.prop.{uid}.value"), dr, &prop.name);
|
||||
let mut opts = vec!["None".to_string()];
|
||||
opts.extend(comp.layers.iter().enumerate().map(|(i, l)| format!("{}. {}", i + 1, l.name)));
|
||||
let cur = l.and_then(idx_of).map(|i| i + 1).unwrap_or(0);
|
||||
if let Some(i) = widgets::popup_menu(ui, pop, dr.left_bottom(), &opts, Some(cur)) {
|
||||
set(actions, if i == 0 { serde_json::Value::Null } else { json!(comp.layers[i - 1].id.0) });
|
||||
}
|
||||
// Source / Masks / Effects & Masks (the hidden companion parameter).
|
||||
if let Some(src) = group.get(&effectcraft_engine::effects::layer_source_id(&prop.match_id)) {
|
||||
let si = src.value.as_enum() as usize;
|
||||
|
||||
@@ -4233,13 +4233,16 @@ fn value_editor(
|
||||
}
|
||||
Value::Text(_) => {}
|
||||
Value::Layer(l) => {
|
||||
p.text(
|
||||
pos2(x, at.y),
|
||||
Align2::LEFT_CENTER,
|
||||
l.map(|l| crate::i18n::tr_args("Layer {}", &[&l])).unwrap_or(crate::i18n::tr("None").into()),
|
||||
Tokens::ui(12.0),
|
||||
t.hot_text,
|
||||
);
|
||||
// Layer references (a light's Source, a Displacement Map's layer…) pick from the
|
||||
// comp's layers, as in Effect Controls; the choice is written through the same
|
||||
// undoable prop.set as the other value arms.
|
||||
let r = Rect::from_min_size(pos2(x, at.y - 9.0), vec2(150.0, 18.0));
|
||||
if let Some(comp) = app.session.active_comp_arc()
|
||||
&& let Some(chosen) = widgets::layer_picker(ui, r, &comp.layers, *l, &t, egui::Id::new(("tl-lay", uid)))
|
||||
{
|
||||
set(actions, chosen.map(|id| json!(id)).unwrap_or(serde_json::Value::Null));
|
||||
}
|
||||
app.auto.add(&format!("timeline.prop.{uid}.value"), r, &prop.name);
|
||||
}
|
||||
Value::Gradient(_) => {
|
||||
// Opens the Gradient Editor at the current time (#612).
|
||||
|
||||
@@ -557,6 +557,44 @@ pub fn popup_menu(ui: &mut Ui, id: egui::Id, pos: egui::Pos2, options: &[String]
|
||||
})
|
||||
}
|
||||
|
||||
/// The drop-down for a layer-valued property (`ParamUi::Layer`): the current layer shown as
|
||||
/// "N. name" (or "None"), and a popup of "None" plus every layer of the comp. Effect Controls
|
||||
/// and the Timeline share it so a layer reference (an Environment light's Source, a Displacement
|
||||
/// Map's layer) is picked the same way in every panel, and the reference is shown as its layer's
|
||||
/// index and name rather than its internal id. Returns `Some(chosen)` when the user picked —
|
||||
/// the layer's id, or `None` for "None" — and `None` while nothing is chosen; the caller writes
|
||||
/// it as an undoable `prop.set` (id, or null for "None").
|
||||
pub fn layer_picker(
|
||||
ui: &mut Ui,
|
||||
rect: Rect,
|
||||
layers: &[effectcraft_engine::project::Layer],
|
||||
current: Option<u64>,
|
||||
t: &Tokens,
|
||||
id: egui::Id,
|
||||
) -> Option<Option<u64>> {
|
||||
let index_of = |wanted: u64| layers.iter().position(|l| l.id.0 == wanted);
|
||||
let entry = |i: usize| layers.get(i).map(|l| format!("{}. {}", i + 1, l.name));
|
||||
let current_index = current.and_then(index_of);
|
||||
let label = current_index.and_then(entry).unwrap_or_else(|| crate::i18n::tr("None").to_string());
|
||||
let pop = id.with("pop");
|
||||
if dropdown(ui, rect, &label, t, id).clicked() {
|
||||
open_popup(ui, pop);
|
||||
}
|
||||
if !popup_is_open(ui, pop) {
|
||||
return None;
|
||||
}
|
||||
// Built only while open: a comp can hold hundreds of layers.
|
||||
let mut options: Vec<String> = Vec::with_capacity(layers.len() + 1);
|
||||
options.push(crate::i18n::tr("None").to_string());
|
||||
options.extend((0..layers.len()).filter_map(entry));
|
||||
// The current entry is highlighted (the first option is "None").
|
||||
match popup_menu(ui, pop, rect.left_bottom(), &options, Some(current_index.map_or(0, |i| i + 1))) {
|
||||
Some(0) => Some(None),
|
||||
Some(picked) => layers.get(picked - 1).map(|l| Some(l.id.0)),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
//! Picking a layer-valued property with the shared drop-down: the Timeline draws a layer-valued
|
||||
//! Light Options property (an Environment light's Source) as a drop-down of the comp's layers,
|
||||
//! Effect Controls' layer parameters (a Displacement Map's layer) pick through the same widget,
|
||||
//! and both write an undoable `prop.set`.
|
||||
|
||||
use effectcraft_engine::Session;
|
||||
use effectcraft_engine::keyframe::Value;
|
||||
use effectcraft_engine::project::LayerId;
|
||||
use effectcraft_ui_egui::EffectcraftApp;
|
||||
use effectcraft_ui_egui::dock::PanelKind;
|
||||
use egui::{Pos2, Rect, pos2, vec2};
|
||||
use egui_kittest::Harness;
|
||||
use egui_kittest::kittest::NodeT;
|
||||
use egui_kittest::kittest::Queryable;
|
||||
use serde_json::json;
|
||||
|
||||
fn click(h: &mut Harness<'_, EffectcraftApp>, at: Pos2) {
|
||||
h.event(egui::Event::PointerMoved(at));
|
||||
h.step();
|
||||
h.event(egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed: true, modifiers: Default::default() });
|
||||
h.step();
|
||||
h.event(egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed: false, modifiers: Default::default() });
|
||||
h.run_steps(3);
|
||||
}
|
||||
|
||||
fn rect_of(h: &Harness<'_, EffectcraftApp>, id: &str) -> Rect {
|
||||
let e = h.state().auto.find(id).unwrap_or_else(|| panic!("no automation element {id}")).clone();
|
||||
Rect::from_min_size(pos2(e.rect[0], e.rect[1]), vec2(e.rect[2], e.rect[3]))
|
||||
}
|
||||
|
||||
/// Whether `want` is painted inside `row` (the drop-down's current-value text).
|
||||
fn painted_in(h: &Harness<'_, EffectcraftApp>, row: Rect, want: &str) -> bool {
|
||||
h.output().shapes.iter().any(|c| match &c.shape {
|
||||
egui::Shape::Text(t) => t.galley.text() == want && row.expand(3.0).contains(t.pos),
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
|
||||
/// The current value of a layer-valued property, by uid.
|
||||
fn layer_value(h: &Harness<'_, EffectcraftApp>, layer: u64, prop: u64) -> Option<u64> {
|
||||
let comp = h.state().session.active_comp().unwrap();
|
||||
match &comp.layer(LayerId(layer)).unwrap().props.find(prop).unwrap().value {
|
||||
Value::Layer(v) => *v,
|
||||
other => panic!("property {prop} is not layer-valued: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// The "N. name" a drop-down shows for a layer.
|
||||
fn entry_for(h: &Harness<'_, EffectcraftApp>, layer: u64) -> String {
|
||||
let comp = h.state().session.active_comp().unwrap();
|
||||
let i = comp.layers.iter().position(|l| l.id.0 == layer).unwrap();
|
||||
format!("{}. {}", i + 1, comp.layers[i].name)
|
||||
}
|
||||
|
||||
/// Puts the layer-valued `prop` of `layer` back to `value` with a direct `prop.set` (no merge key,
|
||||
/// so the pick below records its own step instead of folding into a picker write), picks `none`
|
||||
/// from the drop-down at `value_id`, and checks the clear takes one undo step that restores `value`.
|
||||
#[track_caller]
|
||||
fn none_pick_clears(h: &mut Harness<'_, EffectcraftApp>, value_id: &str, none: &str, layer: u64, prop: u64, value: u64) {
|
||||
h.state_mut().session.execute("prop.set", json!({"layer": layer, "prop": prop, "value": value})).unwrap();
|
||||
h.run_steps(2);
|
||||
assert_eq!(layer_value(h, layer, prop), Some(value));
|
||||
let steps = h.state().session.history.undo.len();
|
||||
let at = rect_of(h, value_id).center();
|
||||
click(h, at);
|
||||
h.get_by_label(none).click();
|
||||
h.run_steps(3);
|
||||
assert_eq!(layer_value(h, layer, prop), None, "selecting None clears the value");
|
||||
assert_eq!(h.state().session.history.undo.len(), steps + 1, "one clear undo step");
|
||||
h.state_mut().session.execute("edit.undo", json!({})).unwrap();
|
||||
h.run_steps(2);
|
||||
assert_eq!(layer_value(h, layer, prop), Some(value), "undo restores the selected layer");
|
||||
}
|
||||
|
||||
/// A light's Source (a layer-valued Light Options property) picks a comp layer from the
|
||||
/// Timeline and undo restores None.
|
||||
#[test]
|
||||
fn timeline_source_drop_down_picks_the_layer_and_undo_restores_none() {
|
||||
let mut s = Session::default();
|
||||
s.execute("comp.new", json!({"name": "Main", "width": 640, "height": 360, "frameRate": 30, "duration": 4})).unwrap();
|
||||
s.execute("comp.settings", json!({"renderer": "advanced3D"})).unwrap();
|
||||
let sky = s.execute("layer.newSolid", json!({"name": "Sky HDRI", "color": "#66aaff"})).unwrap()["layer"].as_u64().unwrap();
|
||||
let light = s.execute("layer.newLight", json!({"kind": "Environment", "name": "Env Light"})).unwrap()["layer"].as_u64().unwrap();
|
||||
let (group, source) = {
|
||||
let comp = s.active_comp().unwrap();
|
||||
let opts = comp.layer(LayerId(light)).unwrap().props.sub("lightOptions").unwrap();
|
||||
(opts.uid, opts.get("source").unwrap().uid)
|
||||
};
|
||||
let mut app = EffectcraftApp::new(s);
|
||||
app.ui.timeline.open_layers.insert(light);
|
||||
app.ui.timeline.open_groups.insert(group);
|
||||
let mut h = Harness::builder().with_size(vec2(1600.0, 1000.0)).build_eframe(|_| app);
|
||||
h.run_steps(4);
|
||||
|
||||
let value_id = format!("timeline.prop.{source}.value");
|
||||
// Unset: the row is a drop-down reading "None" — an element to click, not bare text.
|
||||
assert_eq!(layer_value(&h, light, source), None);
|
||||
assert!(painted_in(&h, rect_of(&h, &value_id), "None"), "the Source row shows a None drop-down");
|
||||
|
||||
// Opening it lists "None" and every layer of the comp, by index and name.
|
||||
let at = rect_of(&h, &value_id).center();
|
||||
click(&mut h, at);
|
||||
let entry = entry_for(&h, sky);
|
||||
assert!(h.query_by_label("None").is_some(), "the list starts with None");
|
||||
assert!(h.query_by_label(&entry).is_some(), "the comp's layers are listed ({entry})");
|
||||
|
||||
let steps = h.state().session.history.undo.len();
|
||||
h.get_by_label(&entry).click();
|
||||
h.run_steps(3);
|
||||
assert_eq!(layer_value(&h, light, source), Some(sky), "the Source holds the layer id");
|
||||
assert_eq!(h.state().session.history.undo.len(), steps + 1, "one undo step");
|
||||
// The row names the layer (index and name), not its internal id.
|
||||
assert!(painted_in(&h, rect_of(&h, &value_id), &entry), "the Source row shows the layer's name");
|
||||
|
||||
// Dismissing the list without choosing writes nothing; reopening marks the chosen layer as
|
||||
// the current entry (and "None" as not current).
|
||||
click(&mut h, pos2(1500.0, 300.0));
|
||||
h.run_steps(2);
|
||||
assert_eq!(layer_value(&h, light, source), Some(sky), "dismissing the list writes nothing");
|
||||
let reopen = rect_of(&h, &value_id).center();
|
||||
click(&mut h, reopen);
|
||||
assert_eq!(h.get_by_label(&entry).accesskit_node().toggled(), Some(egui::accesskit::Toggled::True), "the current entry is highlighted");
|
||||
assert_eq!(h.get_by_label("None").accesskit_node().toggled(), Some(egui::accesskit::Toggled::False), "None is not the current entry");
|
||||
click(&mut h, pos2(1500.0, 300.0));
|
||||
h.run_steps(2);
|
||||
|
||||
h.state_mut().session.execute("edit.undo", json!({})).unwrap();
|
||||
h.run_steps(2);
|
||||
assert_eq!(layer_value(&h, light, source), None, "undo restores None");
|
||||
|
||||
none_pick_clears(&mut h, &value_id, "None", light, source, sky);
|
||||
}
|
||||
|
||||
/// The shared widget is the one Effect Controls already used: a Displacement Map's layer
|
||||
/// parameter still picks from the drop-down and still undoes. The interface language is
|
||||
/// Japanese: "None" is the catalog's word there, like every other label drawn in the panel.
|
||||
#[test]
|
||||
fn effect_controls_layer_param_picks_through_the_shared_widget() {
|
||||
let mut s = Session::default();
|
||||
s.prefs.general.language = "ja".to_string();
|
||||
s.execute("comp.new", json!({"name": "Main", "width": 640, "height": 360, "frameRate": 30, "duration": 4})).unwrap();
|
||||
let map = s.execute("layer.newSolid", json!({"name": "Disp", "color": "#ff0000"})).unwrap()["layer"].as_u64().unwrap();
|
||||
let base = s.execute("layer.newSolid", json!({"name": "Base", "color": "#20c040"})).unwrap()["layer"].as_u64().unwrap();
|
||||
let fx = s.execute("effect.apply", json!({"layers": [base], "effect": "Displacement Map"})).unwrap()["effects"][0].as_u64().unwrap();
|
||||
let prop = {
|
||||
let comp = s.active_comp().unwrap();
|
||||
comp.layer(LayerId(base)).unwrap().effects().unwrap().find_group(fx).unwrap().get("displacementMapLayer").unwrap().uid
|
||||
};
|
||||
s.state.selected_layers = vec![LayerId(base)];
|
||||
let mut app = EffectcraftApp::new(s);
|
||||
app.show_panel(PanelKind::EffectControls);
|
||||
let mut h = Harness::builder().with_size(vec2(1600.0, 1100.0)).build_eframe(|_| app);
|
||||
h.run_steps(4);
|
||||
|
||||
let value_id = format!("effectControls.prop.{prop}.value");
|
||||
// The closed drop-down is the translated "None" (なし), not a hardcoded English word.
|
||||
let row = rect_of(&h, &value_id);
|
||||
assert!(painted_in(&h, row, "なし"), "the parameter row shows the translated None");
|
||||
click(&mut h, row.center());
|
||||
let entry = entry_for(&h, map);
|
||||
assert!(h.query_by_label(&entry).is_some(), "the comp's layers are listed ({entry})");
|
||||
let steps = h.state().session.history.undo.len();
|
||||
h.get_by_label(&entry).click();
|
||||
h.run_steps(3);
|
||||
assert_eq!(layer_value(&h, base, prop), Some(map), "the parameter holds the layer id");
|
||||
assert_eq!(h.state().session.history.undo.len(), steps + 1, "one undo step");
|
||||
// The row names the picked layer, as the Timeline's Source row does.
|
||||
assert!(painted_in(&h, rect_of(&h, &value_id), &entry), "the parameter row shows the layer's name");
|
||||
h.state_mut().session.execute("edit.undo", json!({})).unwrap();
|
||||
h.run_steps(2);
|
||||
assert_eq!(layer_value(&h, base, prop), None, "undo restores None");
|
||||
|
||||
none_pick_clears(&mut h, &value_id, "なし", base, prop, map);
|
||||
}
|
||||
|
||||
/// The visual record (the `ui_3d` ignored-snapshot convention): the Source drop-down open over
|
||||
/// the layer list, and the row after a layer is chosen. Renders with wgpu when `EC_SNAPSHOT_DIR`
|
||||
/// is set: `EC_SNAPSHOT_DIR=shots cargo test -p effectcraft-ui-egui --test ui_layer_source_picker -- --ignored`.
|
||||
#[test]
|
||||
#[ignore = "renders with wgpu; run with --ignored (set EC_SNAPSHOT_DIR to keep PNGs)"]
|
||||
fn snapshot_source_dropdown_open_and_chosen() {
|
||||
let mut s = Session::default();
|
||||
s.execute("comp.new", json!({"name": "Main", "width": 1280, "height": 720, "frameRate": 30, "duration": 5})).unwrap();
|
||||
s.execute("comp.settings", json!({"renderer": "advanced3D"})).unwrap();
|
||||
let sky = s.execute("layer.newSolid", json!({"name": "Sky HDRI", "color": "#66aaff"})).unwrap()["layer"].as_u64().unwrap();
|
||||
let light = s.execute("layer.newLight", json!({"kind": "Environment", "name": "Env Light"})).unwrap()["layer"].as_u64().unwrap();
|
||||
let (group, source) = {
|
||||
let comp = s.active_comp().unwrap();
|
||||
let opts = comp.layer(LayerId(light)).unwrap().props.sub("lightOptions").unwrap();
|
||||
(opts.uid, opts.get("source").unwrap().uid)
|
||||
};
|
||||
let mut app = EffectcraftApp::new(s);
|
||||
app.ui.timeline.open_layers.insert(light);
|
||||
app.ui.timeline.open_groups.insert(group);
|
||||
let mut h = Harness::builder().with_size(vec2(1600.0, 1000.0)).with_pixels_per_point(2.0).wgpu().build_eframe(|_| app);
|
||||
h.run_steps(4);
|
||||
let save = |h: &mut Harness<'_, EffectcraftApp>, name: &str| {
|
||||
if let Ok(dir) = std::env::var("EC_SNAPSHOT_DIR") {
|
||||
h.render().expect("render").save(format!("{dir}/{name}")).expect("save");
|
||||
}
|
||||
};
|
||||
save(&mut h, "source_unset.png");
|
||||
let at = rect_of(&h, &format!("timeline.prop.{source}.value")).center();
|
||||
click(&mut h, at);
|
||||
save(&mut h, "source_dropdown_open.png");
|
||||
let entry = entry_for(&h, sky);
|
||||
h.get_by_label(&entry).click();
|
||||
h.run_steps(3);
|
||||
assert_eq!(layer_value(&h, light, source), Some(sky));
|
||||
save(&mut h, "source_chosen.png");
|
||||
}
|
||||
Reference in New Issue
Block a user