mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-01 05:58:19 -06:00
Timeline: Tab moves to the next value field (#361)
Tab in a value being typed (hot text) dropped the keyboard: egui moved the focus to the next widget, and the inline editors asked for the focus again every frame, which also reset what keys a field keeps for itself. As in After Effects, Tab now commits the value and types in the next value field of the same panel (the next dimension, then the next property's), Shift+Tab the previous one. The hot-text fields of each panel register in drawing order; the dock tags each panel's Ui so the order stays within it. `keep_focus` asks for the focus only when the field doesn't have it. Tests: tab_commits_a_value_and_moves_to_the_next (ui_inline_edit: Timeline and Effect Controls). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
0171eb1e2e
commit
9c9e954ee2
@@ -159,7 +159,9 @@ impl EffectcraftApp {
|
||||
panels::precomp::navigator(self, &mut child, nav);
|
||||
content.min.y = nav.max.y;
|
||||
}
|
||||
let mut child = ui.new_child(egui::UiBuilder::new().max_rect(content).id_salt(("panel", p.id())));
|
||||
let mut child = ui.new_child(
|
||||
egui::UiBuilder::new().max_rect(content).id_salt(("panel", p.id())).ui_stack_info(egui::UiStackInfo::default().with_tag(crate::widgets::PANEL_TAG)),
|
||||
);
|
||||
child.set_clip_rect(content.intersect(ui.clip_rect()));
|
||||
panels::show(self, &mut child, p, content);
|
||||
if viewer && !self.ui.start_screen {
|
||||
|
||||
@@ -6,7 +6,49 @@ use crate::automation::Registry;
|
||||
use crate::icons::{self, Icon};
|
||||
use crate::theme::Tokens;
|
||||
|
||||
/// "Hot text": a blue number you drag horizontally to scrub, click to type.
|
||||
/// The [`egui::UiStackInfo`] tag of a dock panel's `Ui`: Tab moves between the hot-text fields
|
||||
/// of one panel.
|
||||
pub const PANEL_TAG: &str = "effectcraft.panel";
|
||||
|
||||
/// A panel's hot-text fields in drawing order (this pass's and the last one's), and a Tab
|
||||
/// pressed while typing in one: (that field, Shift held, the pass).
|
||||
#[derive(Clone, Default)]
|
||||
struct TabOrder {
|
||||
pass: u64,
|
||||
current: Vec<egui::Id>,
|
||||
last: Vec<egui::Id>,
|
||||
pending: Option<(egui::Id, bool, u64)>,
|
||||
}
|
||||
|
||||
fn tab_order_id(ui: &Ui) -> egui::Id {
|
||||
let panel = ui.stack().iter().find(|s| s.tags().contains(PANEL_TAG)).map_or(egui::Id::NULL, |s| s.id);
|
||||
egui::Id::new(("hot-tab-order", panel))
|
||||
}
|
||||
|
||||
/// Puts hot-text field `id` in its panel's Tab order. True when a Tab in the field before it
|
||||
/// (Shift+Tab: after it) moves the typing here.
|
||||
fn tab_stop(ui: &Ui, id: egui::Id) -> bool {
|
||||
let key = tab_order_id(ui);
|
||||
let pass = ui.ctx().cumulative_pass_nr();
|
||||
ui.data_mut(|d| {
|
||||
let o = d.get_temp_mut_or_default::<TabOrder>(key);
|
||||
if o.pass != pass {
|
||||
o.last = std::mem::take(&mut o.current);
|
||||
o.pass = pass;
|
||||
}
|
||||
o.current.push(id);
|
||||
let Some((from, back, at)) = o.pending else { return false };
|
||||
let i = o.last.iter().position(|f| *f == from);
|
||||
let next = i.and_then(|i| if back { i.checked_sub(1) } else { i.checked_add(1) }).and_then(|i| o.last.get(i)).copied();
|
||||
if next == Some(id) || next.is_none() || pass > at.saturating_add(2) {
|
||||
o.pending = None;
|
||||
}
|
||||
next == Some(id)
|
||||
})
|
||||
}
|
||||
|
||||
/// "Hot text": a blue number you drag horizontally to scrub, click to type. Tab / Shift+Tab
|
||||
/// while typing commits and types in the panel's next / previous one, as in After Effects.
|
||||
/// Returns (response, Some(new value) when changed, drag finished).
|
||||
pub fn hot_number_at(
|
||||
ui: &mut Ui,
|
||||
@@ -20,16 +62,28 @@ pub fn hot_number_at(
|
||||
t: &Tokens,
|
||||
) -> (Rect, Option<f64>, bool) {
|
||||
let editing_id = id.with("editing");
|
||||
let editing: Option<String> = ui.data(|d| d.get_temp(editing_id));
|
||||
let mut editing: Option<String> = ui.data(|d| d.get_temp(editing_id));
|
||||
if tab_stop(ui, id) && editing.is_none() {
|
||||
editing = Some(format!("{value:.decimals$}"));
|
||||
}
|
||||
let text = format!("{value:.decimals$}{suffix}");
|
||||
let galley = ui.painter().layout_no_wrap(text, Tokens::ui(12.0), t.hot_text);
|
||||
let rect = Rect::from_min_size(rect_min, galley.size() + vec2(4.0, 4.0));
|
||||
if let Some(mut buf) = editing {
|
||||
let er = Rect::from_min_size(rect_min - vec2(2.0, 1.0), vec2((galley.size().x + 24.0).max(54.0), 18.0));
|
||||
let mut child = ui.new_child(egui::UiBuilder::new().max_rect(er));
|
||||
let r = child.add(egui::TextEdit::singleline(&mut buf).desired_width(er.width()).font(Tokens::ui(12.0)).margin(egui::Margin::symmetric(2, 1)));
|
||||
// (Tab stays with the field: it moves to the next hot-text field, not the next widget.)
|
||||
let r = child.add(
|
||||
egui::TextEdit::singleline(&mut buf)
|
||||
.id(id.with("text"))
|
||||
.desired_width(er.width())
|
||||
.font(Tokens::ui(12.0))
|
||||
.margin(egui::Margin::symmetric(2, 1))
|
||||
.lock_focus(true),
|
||||
);
|
||||
let tab = r.has_focus().then(|| ui.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::Tab).then_some(i.modifiers.shift))).flatten();
|
||||
let mut out = None;
|
||||
if r.lost_focus() {
|
||||
if r.lost_focus() || tab.is_some() {
|
||||
let txt = buf.trim().trim_end_matches(suffix).trim().replace(',', ".");
|
||||
if !ui.input(|i| i.key_pressed(egui::Key::Escape))
|
||||
&& let Ok(v) = txt.parse::<f64>()
|
||||
@@ -38,6 +92,11 @@ pub fn hot_number_at(
|
||||
out = Some(v.clamp(range.0, range.1));
|
||||
}
|
||||
ui.data_mut(|d| d.remove::<String>(editing_id));
|
||||
if let Some(back) = tab {
|
||||
r.surrender_focus();
|
||||
let (key, pass) = (tab_order_id(ui), ui.ctx().cumulative_pass_nr());
|
||||
ui.data_mut(|d| d.get_temp_mut_or_default::<TabOrder>(key).pending = Some((id, back, pass)));
|
||||
}
|
||||
} else {
|
||||
keep_focus(ui.ctx(), &r, &buf);
|
||||
ui.data_mut(|d| d.insert_temp(editing_id, buf));
|
||||
@@ -97,11 +156,13 @@ pub fn hot_int_at(ui: &mut Ui, rect_min: egui::Pos2, id: egui::Id, value: i64, s
|
||||
|
||||
/// Keep the keyboard on an inline editor that is open until it loses the focus. The frame it
|
||||
/// opens (it has no focus yet) its whole `text` is selected, so typing replaces the value.
|
||||
/// (Only then is the focus requested: a request resets what keys the field keeps for itself,
|
||||
/// such as Tab with [`egui::TextEdit::lock_focus`].)
|
||||
pub fn keep_focus(ctx: &egui::Context, field: &Response, text: &str) {
|
||||
if !ctx.memory(|m| m.has_focus(field.id)) {
|
||||
select_all(ctx, field.id, text);
|
||||
field.request_focus();
|
||||
}
|
||||
field.request_focus();
|
||||
}
|
||||
|
||||
/// Select all of the text field `id`'s `text`, so typing replaces it.
|
||||
|
||||
@@ -262,6 +262,74 @@ fn the_expression_field_grows_while_editing_and_drags_taller() {
|
||||
assert_eq!(h.state().ui.timeline.expr_lines.get(&opacity), Some(&1));
|
||||
}
|
||||
|
||||
fn key_with(h: &mut Harness<'_, EffectcraftApp>, key: Key, modifiers: Modifiers) {
|
||||
h.input_mut().events.push(Event::ModifiersChanged(modifiers));
|
||||
h.input_mut().events.push(Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers });
|
||||
h.input_mut().events.push(Event::Key { key, physical_key: None, pressed: false, repeat: false, modifiers });
|
||||
h.step();
|
||||
h.input_mut().events.push(Event::ModifiersChanged(Modifiers::NONE));
|
||||
h.run_steps(2);
|
||||
}
|
||||
|
||||
/// #361: Tab in a value being typed commits it and types in the next value (the next dimension,
|
||||
/// then the next property's), Shift+Tab the previous, in the Timeline and in Effect Controls,
|
||||
/// as in After Effects. It used to drop the keyboard instead.
|
||||
#[test]
|
||||
fn tab_commits_a_value_and_moves_to_the_next() {
|
||||
let (mut h, _) = harness();
|
||||
let (layer, transform) = {
|
||||
let s = &mut h.state_mut().session;
|
||||
let l = s.execute("layer.newSolid", json!({"name": "Plate", "color": "#406080"})).unwrap()["layer"].as_u64().unwrap();
|
||||
s.execute("layer.select", json!({"layers": [l]})).unwrap();
|
||||
for fx in ["Point Control", "Slider Control"] {
|
||||
s.execute("effect.apply", json!({"layers": [l], "effect": fx})).unwrap();
|
||||
}
|
||||
let tr = s.active_comp().unwrap().layer(effectcraft_engine::project::LayerId(l)).unwrap().props.sub("transform").unwrap().uid;
|
||||
(l, tr)
|
||||
};
|
||||
h.state_mut().ui.timeline.open_layers.insert(layer);
|
||||
h.state_mut().ui.timeline.open_groups.insert(transform);
|
||||
h.state_mut().show_panel(effectcraft_ui_egui::dock::PanelKind::EffectControls);
|
||||
h.run_steps(3);
|
||||
let props =
|
||||
|h: &Harness<'_, EffectcraftApp>| h.state().session.active_comp().unwrap().layer(effectcraft_engine::project::LayerId(layer)).unwrap().props.clone();
|
||||
let value = |h: &Harness<'_, EffectcraftApp>, path: &str| props(h).prop(path).unwrap().value.components();
|
||||
let uid = |h: &Harness<'_, EffectcraftApp>, path: &str| props(h).prop(path).unwrap().uid;
|
||||
let (position, scale) = (uid(&h, "transform/position"), uid(&h, "transform/scale"));
|
||||
let none = Modifiers::NONE;
|
||||
// Timeline: Position X, Tab, Position Y, Tab, Scale (linked).
|
||||
let p = center(&h, &format!("timeline.prop.{position}.value.0"));
|
||||
click(&mut h, p, 1);
|
||||
type_text(&mut h, "100");
|
||||
key_with(&mut h, Key::Tab, none);
|
||||
type_text(&mut h, "50");
|
||||
key_with(&mut h, Key::Tab, none);
|
||||
type_text(&mut h, "200");
|
||||
key(&mut h, Key::Enter);
|
||||
assert_eq!(value(&h, "transform/position")[..2], [100.0, 50.0]);
|
||||
assert_eq!(value(&h, "transform/scale")[..2], [200.0, 200.0]);
|
||||
assert!(!h.ctx.egui_wants_keyboard_input(), "Enter ends the typing");
|
||||
// Shift+Tab: from Scale X back to Position Y.
|
||||
let p = center(&h, &format!("timeline.prop.{scale}.value.0"));
|
||||
click(&mut h, p, 1);
|
||||
type_text(&mut h, "120");
|
||||
key_with(&mut h, Key::Tab, Modifiers::SHIFT);
|
||||
type_text(&mut h, "60");
|
||||
key(&mut h, Key::Enter);
|
||||
assert_eq!(value(&h, "transform/position")[..2], [100.0, 60.0]);
|
||||
assert_eq!(value(&h, "transform/scale")[..2], [120.0, 120.0]);
|
||||
// Effect Controls: Point Control's Y, Tab, Slider Control's Slider (the next effect).
|
||||
let point = uid(&h, "effects/#1/point");
|
||||
let p = center(&h, &format!("effectControls.prop.{point}.value.1"));
|
||||
click(&mut h, p, 1);
|
||||
type_text(&mut h, "7");
|
||||
key_with(&mut h, Key::Tab, none);
|
||||
type_text(&mut h, "42");
|
||||
key(&mut h, Key::Enter);
|
||||
assert_eq!(value(&h, "effects/#1/point")[1], 7.0);
|
||||
assert_eq!(value(&h, "effects/#2/slider"), [42.0]);
|
||||
}
|
||||
|
||||
/// A click at `p` with `modifiers` held.
|
||||
fn click_with(h: &mut Harness<'_, EffectcraftApp>, p: Pos2, modifiers: Modifiers) {
|
||||
h.input_mut().events.push(Event::PointerMoved(p));
|
||||
|
||||
+1
-1
@@ -171,7 +171,7 @@ the disk cache in the formats wave; the 3D Camera Tracker in M12.6.)
|
||||
| Web | 98% | 0.1 | No shared-memory threads inside one engine instance (the decided design is one engine instance per worker; a threaded build needs nightly `build-std`) (M13.30: Render Queue, analyses and Content-Aware Fill render on each job worker's own WebGPU device in passes, particles and Advanced 3D read back under keys, layer buffers in the browser's disk cache; browser storage, Web Audio, Web Worker renders/analyses, WebGPU viewer and offline install landed in M15.2; viewer frames in frame workers fed by project diffs, Roto Brush propagation in a worker, non-blocking `wait: true` jobs and a browser Media Browser (File System Access folders, browser storage) in M13.10; WEB-1 in M13.24: GPU effects and the GPU compositor in the frame workers on their own WebGPU devices with deferred readbacks (frames render in passes; Backend Auto per comp), the disk cache in the Origin Private File System (written by the workers with sync access handles, LRU under the settings' limit, served after a reload), the storage manager (Settings ▸ Disk ▸ Browser Storage, `storage.info` / `storage.persist` / `storage.clear`), and a WGSL constant Chrome rejected (it disabled every GPU kernel in Chrome) fixed) |
|
||||
| 3D | 88% | 5.5 | reflections of other layers (Advanced 3D surfaces reflect only the Environment light; Material Options ▸ Appears in Reflections is stored but has no visible effect yet, #261), multi-view layouts, the Extended Viewer for Advanced 3D comps (Classic 3D Extended Viewer landed in M13.5 UI completion); collapsed precomps of another size seen through the parent's camera render (fixed in M13.2); stereo rigs, orbit nulls, lights controlled by the camera, cameras/lights from glTF models, environment backgrounds, Advanced 3D motion blur, blend modes and track mattes landed in M7.7; Classic 3D iris-shaped bokeh with highlights, progressive depth of field on tilted layers and the focus-link commands landed in M13.5; Advanced 3D depth of field with the iris and highlight options, collapsed precomps as real Advanced 3D geometry and extruded text/shape strokes landed in M13.8; Advanced 3D (glTF/OBJ models, primitives, extruded text and shapes, PBR, image-based light, shadow maps, GPU rasteriser) in M7.4–M7.6; Advanced 3D end to end on the GPU (motion blur, iris depth of field, compositing) in M13.11 |
|
||||
| Effects | ≈ 85% | 3.0 | GPU versions of the remaining effects (280 run on the GPU since M13.29, EFF-5: M13.28 added the remaining pixel effects and the geometry and audio generators (Lightning, Advanced Lightning, Beam, Lens Flare, Radio Waves, Vegas, Stroke, Scribble, Write-on, Paint Bucket, Eyedropper Fill, CC Glue Gun, CC Threads, Audio Spectrum / Waveform, Basic / Path Text); M13.29 added the particle effects (CC Particle World / Systems II, Particle Playground, CC Ball Action, CC Pixel Polly, CC Scatterize, Curl Noise) and removed the fallbacks of Shatter's wireframe views, Foam's extras, Timewarp's Matte Layer, strong Warp Fisheye / Twist bends and custom `.ocio` configs; M13.23 added the 3D Channel and Immersive Video families, Apply Color LUT, the OCIO effects, Color Profile Converter and the simulations' render passes (Card Wipe included); M13.22 added the CC light family, the CC transitions, Block Dissolve, Radial Shadow, CC Bender / Blobbylize / Cylinder / Sphere / Spotlight / Environment, 3D Glasses, Numbers, Timecode and the time effects joined the 166 of M13.13; Fractal and the analysis / tool effects still render on the CPU) and the missing controls listed as partial in [effects.md](effects.md) (every After Effects effect exists since M9.11, M12.5 and M12.6; parameter names, order, twirl-downs, popups, units and defaults were aligned in M9.12) |
|
||||
| Interface | 75% | 5.0 | more Learn tutorials and pixel-level fidelity of dialogs (G2 on 9 October: the viewer's resize cursors point the way each handle scales and turn with the layer, #393; Ctrl/Cmd-click on the Timeline's or the Composition panel's current-time display toggles Timecode / Frames, #370; the in-window menu bar works from the keyboard (Alt, arrows, Enter, Escape) and the app's shortcuts wait while a menu is open, #279; the Timeline's composition marker bin and numpad * add comp markers, #275; Settings ▸ Appearance ▸ UI Scale (75–200 %, beyond After Effects, which follows the system's scaling), #284; G2 on 7 October: Project items, files and effects dropped on the Composition viewer, Project items dropped in the Timeline between layers and at a time, #85, #88, #89; popup menus moved to fit the window take clicks, so Render Queue templates apply, #117; View ▸ New Viewer landed in M13.32; an automation id on every control of Composition and Solid Settings, Group / Ungroup Shapes in the Layer menu, Window ▸ Learn, the flowchart shortcuts and Reveal in Explorer landed in M3.10 and M3.13; the Home ▸ Templates gallery (eight original built-in templates, user templates from File ▸ Save as Template…) and View ▸ Simulate Output ▸ My Custom RGB… landed in M13.25; Timeline layer reordering by drag, a non-snapping viewer pan, a working rename field and twirl arrows, and the full set of property reveal shortcuts — double presses, Alt+Shift keyframes, Ctrl+` — landed in M13.17–M13.20; the Home ▸ Learn tab with interactive tutorials and a UI fidelity pass landed in M13.5 UI completion; visual editors for Lumetri RGB / hue-saturation curves, Colorama's output cycle wheel, Glow's colour map and Reshape's correspondence points (viewer handles), a scrolling Preview panel and AE-style Composition / Timeline tabs (close, label swatch, viewer lock) landed in M13.10; Timeline outline and Project panel columns scroll horizontally, the Layer Style dialog, ROI resize handles, Pan Behind snapping and 3D Reference Axes landed in M13.5; native macOS menu bar, Timeline columns/search/reveal-add, Home screen with recent projects and all AE workspaces landed; viewer rulers/snapping/channels/snapshots landed in M0.13) |
|
||||
| Interface | 75% | 5.0 | more Learn tutorials and pixel-level fidelity of dialogs (G2 on 9 October: the viewer's resize cursors point the way each handle scales and turn with the layer, #393; Ctrl/Cmd-click on the Timeline's or the Composition panel's current-time display toggles Timecode / Frames, #370; Tab / Shift+Tab in a value being typed commits it and types in the next / previous value of the panel (Timeline, Effect Controls), #361; the in-window menu bar works from the keyboard (Alt, arrows, Enter, Escape) and the app's shortcuts wait while a menu is open, #279; the Timeline's composition marker bin and numpad * add comp markers, #275; Settings ▸ Appearance ▸ UI Scale (75–200 %, beyond After Effects, which follows the system's scaling), #284; G2 on 7 October: Project items, files and effects dropped on the Composition viewer, Project items dropped in the Timeline between layers and at a time, #85, #88, #89; popup menus moved to fit the window take clicks, so Render Queue templates apply, #117; View ▸ New Viewer landed in M13.32; an automation id on every control of Composition and Solid Settings, Group / Ungroup Shapes in the Layer menu, Window ▸ Learn, the flowchart shortcuts and Reveal in Explorer landed in M3.10 and M3.13; the Home ▸ Templates gallery (eight original built-in templates, user templates from File ▸ Save as Template…) and View ▸ Simulate Output ▸ My Custom RGB… landed in M13.25; Timeline layer reordering by drag, a non-snapping viewer pan, a working rename field and twirl arrows, and the full set of property reveal shortcuts — double presses, Alt+Shift keyframes, Ctrl+` — landed in M13.17–M13.20; the Home ▸ Learn tab with interactive tutorials and a UI fidelity pass landed in M13.5 UI completion; visual editors for Lumetri RGB / hue-saturation curves, Colorama's output cycle wheel, Glow's colour map and Reshape's correspondence points (viewer handles), a scrolling Preview panel and AE-style Composition / Timeline tabs (close, label swatch, viewer lock) landed in M13.10; Timeline outline and Project panel columns scroll horizontally, the Layer Style dialog, ROI resize handles, Pan Behind snapping and 3D Reference Axes landed in M13.5; native macOS menu bar, Timeline columns/search/reveal-add, Home screen with recent projects and all AE workspaces landed; viewer rulers/snapping/channels/snapshots landed in M0.13) |
|
||||
| Project | ≈ 68% | 5.5 | OCIO displays beyond the built-in tone map; relative footage paths and relinking when a project moves (unsaved-changes prompts, a modified mark that follows undo and project files that name their version and always reopen landed in M3.9 and M3.14; auto-save and crash recovery in M3.7; Color Engine with OCIO/ACES working spaces, HDR compand/tone mapping, Rec. 2100 PQ/HLG output, Feet + Frames, display colour management, Simulate Output and the locked viewer landed in M7.7; proxies and Interpret Footage fields / pixel aspect / alpha guess landed: PRJ-8, PRJ-3; image sequence import options (Sequence, Force Alphabetical Order, frame rate, picked ranges, folders, the browser), missing-frame placeholders and Interpret Footage Start Timecode landed: #297) |
|
||||
| Masks & roto | 74% | 5.0 | (audit estimates; Roto Brush 2.0 / 3.0's trained model, MobileSAM in the swappable `segment` module, has since landed in M13.35; variable-width mask feather points with the Mask Feather tool landed in M13.5; mask tracking and Mask Interpolation landed in M6.6; Roto Brush & Refine Edge with graph-cut segmentation, flow propagation, edge matting, decontamination and Freeze in M6.7) |
|
||||
| Preview | 77% | 2.8 | GPU effects for the audio-driven effects and the stepped particle systems; content-keyed RAM preview frames (an edit drops every comp's), audio scrubbing, more snapshot slots (G2 on 7 October: Audio, Lock and Shy switches keep the cached frames and playback with audio waits for frames instead of skipping them, #103; running out of video memory renders the frame on the CPU and keeps fewer GPU frames instead of crashing, #106; RAM preview keys with the render options, least-recently-shown eviction, Auto resolution while playing, Cache Before Playback that fits, an achieved-fps readout, Cache Frames When Idle and purges that clear what they name landed in M4.5–M4.8; 3D channel, VR, OCIO, Card Wipe and the simulations' render passes run on the GPU since M13.23; Advanced 3D layers with blend modes / track mattes / Preserve Transparency and environment backgrounds composite on the GPU since M13.22; pooled GPU textures and fused quantisation since M13.13: Lower Third GPU warm 21 → 4 ms/frame, and Auto now picks the CPU or the GPU per comp from measured frame times; Advanced 3D runs — raster, motion blur, iris depth of field, compositing — and wireframes run on the GPU since M13.11: CPU warm 3394 → GPU warm 290 ms/frame at 1920×1080 (11.7×; Half 968 → 85 ms), GPU ≠ CPU on 0.005 % of pixels, on an M4 Pro under load; Classic 3D bokeh depth of field — iris shapes, highlights, fringe, progressive blur on tilted planes — runs in WGSL since M13.6: 3D Showcase GPU warm 205 → 54 ms/frame at full size on an M4 Pro; the Preview panel's five shortcuts with their own Include / Loop / Cache Before Playback / Range / Play From / Frame Rate / Skip / Resolution / Full Screen / stop options and `playback.settings.get/set` landed in M13.8; Classic 3D runs and adjustment layers composite on the GPU since M12.7; persistent disk cache with the blue cache bar landed; region of interest, snapshots, exposure and Fast Previews landed in M0.13) |
|
||||
|
||||
Reference in New Issue
Block a user