From e51e2bf04dbc89db4b0d009de6dfa8fa5583aa72 Mon Sep 17 00:00:00 2001 From: Sebastian Benjamin Date: Wed, 19 Aug 2026 14:05:17 -0700 Subject: [PATCH] Let video elements letterbox with #:fit 'contain The default stays cover-crop (right for faces filling a tile), but content whose edges matter - a screen share - needs the whole frame visible, so Kind::Video grows a contain flag mapped to ObjectFit::Contain. Co-Authored-By: Claude Fable 5 --- crates/slag-core/src/decode.rs | 1 + crates/slag-core/src/element.rs | 5 ++++- crates/slag-gpui/src/component.rs | 2 +- crates/slag-gpui/src/materialize.rs | 21 ++++++++++++++------- 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/crates/slag-core/src/decode.rs b/crates/slag-core/src/decode.rs index c25b960..be238c9 100644 --- a/crates/slag-core/src/decode.rs +++ b/crates/slag-core/src/decode.rs @@ -252,6 +252,7 @@ fn build_kind(kind: &str, props: &SteelVal) -> Result { }, "video" => Kind::Video { key: prop_string(props, "#:key").unwrap_or_default(), + contain: prop_string(props, "#:fit").as_deref() == Some("contain"), }, other => return Err(format!("unknown element kind: {other}")), }; diff --git a/crates/slag-core/src/element.rs b/crates/slag-core/src/element.rs index 2124b99..8060cc0 100644 --- a/crates/slag-core/src/element.rs +++ b/crates/slag-core/src/element.rs @@ -249,9 +249,12 @@ pub enum Kind { /// A live video surface. Frames come from the app's video frame source /// (see `UiConfig::video_frames` in slag-gpui), looked up by `key` each /// render; until a frame exists it renders as an empty panel. Size via - /// style w/h. + /// style w/h. `contain` letterboxes the frame (`#:fit 'contain`) instead + /// of the default cover-crop — right for content whose edges matter + /// (screen shares), wrong for faces. Video { key: String, + contain: bool, }, } diff --git a/crates/slag-gpui/src/component.rs b/crates/slag-gpui/src/component.rs index 545d9dd..a337e9d 100644 --- a/crates/slag-gpui/src/component.rs +++ b/crates/slag-gpui/src/component.rs @@ -657,7 +657,7 @@ impl SchemeComponent { fn ensure_videos(&mut self, el: &Element, window: &mut Window) { let mut keys: Vec = Vec::new(); walk_elements(el, &mut |e| { - if let Kind::Video { key } = &e.kind { + if let Kind::Video { key, .. } = &e.kind { keys.push(key.clone()); } }); diff --git a/crates/slag-gpui/src/materialize.rs b/crates/slag-gpui/src/materialize.rs index 4a967d2..a5494cc 100644 --- a/crates/slag-gpui/src/materialize.rs +++ b/crates/slag-gpui/src/materialize.rs @@ -732,7 +732,7 @@ fn build_node( } } Kind::KeyCapture { key, label } => build_key_capture(key, label, &el.style, theme, ctx), - Kind::Video { key } => build_video(el, key, theme, ctx), + Kind::Video { key, contain } => build_video(el, key, *contain, theme, ctx), Kind::Avatar { url, fallback } => { let size = match el.style.w { Some(Dim::Px(v)) => v, @@ -986,14 +986,21 @@ fn build_key_capture( /// The frame is looked up in the component-owned map, not fetched from the app, /// so one render pass presents one consistent frame per key. #[inline(never)] -fn build_video(el: &Element, key: &str, theme: &Theme, ctx: &MaterializeCtx) -> AnyElement { +fn build_video( + el: &Element, + key: &str, + contain: bool, + theme: &Theme, + ctx: &MaterializeCtx, +) -> AnyElement { + let fit = if contain { + ObjectFit::Contain + } else { + ObjectFit::Cover + }; let boxed = apply_style(div(), &el.style, theme).overflow_hidden(); let boxed = match ctx.videos.get(key) { - Some(frame) => boxed.child( - img(frame.clone()) - .size_full() - .object_fit(ObjectFit::Cover), - ), + Some(frame) => boxed.child(img(frame.clone()).size_full().object_fit(fit)), None => boxed.bg(theme.secondary), }; finish_interactive(boxed, el, ctx) -- 2.51.2