diff --git a/crates/gpui/src/dock/dock_area.rs b/crates/gpui/src/dock/dock_area.rs index 85f78bb70c..0f43445504 100644 --- a/crates/gpui/src/dock/dock_area.rs +++ b/crates/gpui/src/dock/dock_area.rs @@ -837,7 +837,7 @@ impl DockArea { let child = child .flex_basis(relative(share)) .flex_grow_0() - .flex_shrink_0(); + .flex_shrink(); container = container.child(child); // One handle per boundary, so every pair of panels can be // resized independently. diff --git a/crates/gpui/src/dock/tab_bar.rs b/crates/gpui/src/dock/tab_bar.rs index d04cf423f5..8657e43089 100644 --- a/crates/gpui/src/dock/tab_bar.rs +++ b/crates/gpui/src/dock/tab_bar.rs @@ -94,6 +94,19 @@ impl TabBar { /// scrolling instead. pub(crate) const MIN_TAB_WIDTH: Pixels = Pixels(64.0); + /// Estimated rendered width of a tab for its title: tabs size to their + /// content (the design shows full `<面板>·<名称>` labels), so drag + /// hit-testing estimates each tab's width from the title — CJK glyphs are + /// full-width, ASCII roughly half — plus the horizontal padding. Only the + /// cached drag geometry uses this; the layout itself measures the text. + fn estimated_width(title: &str) -> Pixels { + let units: f32 = title + .chars() + .map(|ch| if ch.is_ascii() { 0.55 } else { 1.0 }) + .sum(); + Pixels((units * 13.0 + 20.0).max(Self::MIN_TAB_WIDTH.0)) + } + /// Creates a strip for the given tabs; `active` is clamped into range. pub(crate) fn new(tabs: Vec, active: usize) -> Self { Self { @@ -201,16 +214,23 @@ impl Render for TabBar { let colors = cx.default_colors().clone(); // Recompute per-tab geometry from the current order and scroll offset. - self.geometry = self - .tabs - .iter() - .enumerate() - .map(|(index, &panel)| TabGeometry { - panel, - x: Pixels(index as f32 * Self::MIN_TAB_WIDTH.0 - self.scroll_offset.0), - width: Self::MIN_TAB_WIDTH, - }) - .collect(); + // Tabs are content-sized, so accumulate the estimated widths. + { + let mut x = -self.scroll_offset.0; + self.geometry = self + .tabs + .iter() + .enumerate() + .map(|(index, &panel)| { + let width = Self::estimated_width( + self.titles.get(index).map(|t| t.as_ref()).unwrap_or(""), + ); + let tab = TabGeometry { panel, x: Pixels(x), width }; + x += width.0; + tab + }) + .collect(); + } let mut root = div() .flex() @@ -273,9 +293,12 @@ impl Render for TabBar { let mut tab = div() .id(ElementId::named_usize("dock-tab", panel.raw() as usize)) - .w(px(Self::MIN_TAB_WIDTH.0)) + .min_w(px(Self::MIN_TAB_WIDTH.0)) + .px_2() .flex_none() .h_full() + .overflow_hidden() + .whitespace_nowrap() .cursor_pointer() .text_sm() .bg(if active { diff --git a/crates/gpui/src/node_graph/graph_view.rs b/crates/gpui/src/node_graph/graph_view.rs index 6c65b3d109..ded42a46a8 100644 --- a/crates/gpui/src/node_graph/graph_view.rs +++ b/crates/gpui/src/node_graph/graph_view.rs @@ -1011,16 +1011,19 @@ impl Render for NodeGraphView { .on_pinch(cx.listener(|this, event: &PinchEvent, _window, cx| { this.on_scroll_or_pinch(event.position, 1.0 + event.delta, cx); })) - .child(canvas( - move |bounds, _window, cx| { - entity.update(cx, |this, cx| { - this.viewport = bounds; - this.build_draw(cx) - }) - }, - move |bounds, draw: GraphDraw, window, cx| { - NodeGraphView::::paint_draw(&draw, bounds, window, cx); - }, - )) + .child( + canvas( + move |bounds, _window, cx| { + entity.update(cx, |this, cx| { + this.viewport = bounds; + this.build_draw(cx) + }) + }, + move |bounds, draw: GraphDraw, window, cx| { + NodeGraphView::::paint_draw(&draw, bounds, window, cx); + }, + ) + .size_full(), + ) } } diff --git a/crates/gpui/src/timeline/clip.rs b/crates/gpui/src/timeline/clip.rs index 94930254c1..91127a5852 100644 --- a/crates/gpui/src/timeline/clip.rs +++ b/crates/gpui/src/timeline/clip.rs @@ -31,6 +31,11 @@ use super::{ /// Screen-space width of each trim-handle hit zone, in pixels. pub const TRIM_HANDLE_WIDTH: f32 = 6.0; +/// Corner radius of the clip body, per the design's rounded bars. Painted +/// into the body quad itself: gpui's content masks are rectangular only, so +/// a rounded `overflow_hidden` wrapper would not clip the canvas corners. +const CLIP_CORNER_RADIUS: Pixels = px(4.0); + /// Fallback pixels-per-frame used for internal decoration geometry. /// /// [`ClipElement`] receives no zoom (see the type docs), so transition wedge @@ -265,7 +270,7 @@ impl RenderOnce for ClipElement { a: paint.color.a * 0.5, } }; - window.paint_quad(fill(bounds, body_color)); + window.paint_quad(fill(bounds, body_color).corner_radii(CLIP_CORNER_RADIUS)); // Transition wedges: triangles tapering into the clip // from each edge, capped at 40% of the body width so tiny @@ -302,10 +307,16 @@ impl RenderOnce for ClipElement { // State overlays, in back-to-front order. if !paint.enabled { - window.paint_quad(fill(bounds, hsla(0., 0., 0.05, 0.55))); + window.paint_quad( + fill(bounds, hsla(0., 0., 0.05, 0.55)) + .corner_radii(CLIP_CORNER_RADIUS), + ); } if paint.locked { - window.paint_quad(fill(bounds, hsla(0., 0., 0.1, 0.25))); + window.paint_quad( + fill(bounds, hsla(0., 0., 0.1, 0.25)) + .corner_radii(CLIP_CORNER_RADIUS), + ); } // Rich content via the decorator, scoped to the frames @@ -330,11 +341,10 @@ impl RenderOnce for ClipElement { // Selection outline on top of everything. if paint.selected { - window.paint_quad(outline( - bounds, - hsla(0.6, 0.8, 0.6, 1.), - BorderStyle::Solid, - )); + window.paint_quad( + outline(bounds, hsla(0.6, 0.8, 0.6, 1.), BorderStyle::Solid) + .corner_radii(CLIP_CORNER_RADIUS), + ); } }, ) diff --git a/crates/gpui/src/timeline/playhead.rs b/crates/gpui/src/timeline/playhead.rs index 7045e93195..9ff1d8ce4f 100644 --- a/crates/gpui/src/timeline/playhead.rs +++ b/crates/gpui/src/timeline/playhead.rs @@ -71,6 +71,7 @@ impl RenderOnce for PlayheadElement { window.paint_path(path, color); }, ) + .size_full() } } diff --git a/crates/gpui/src/timeline/ruler.rs b/crates/gpui/src/timeline/ruler.rs index 54351b6f6e..f8b804a519 100644 --- a/crates/gpui/src/timeline/ruler.rs +++ b/crates/gpui/src/timeline/ruler.rs @@ -282,6 +282,7 @@ impl RenderOnce for TimelineRuler { } }, ) + .size_full() } } diff --git a/crates/gpui/src/timeline/timeline_view.rs b/crates/gpui/src/timeline/timeline_view.rs index ded6cf5861..a8540f68f8 100644 --- a/crates/gpui/src/timeline/timeline_view.rs +++ b/crates/gpui/src/timeline/timeline_view.rs @@ -753,6 +753,7 @@ impl Render for TimelineView { let decorator = self.decorator.clone(); let ruler = div() + .flex() .flex_row() .h(px(32.)) .flex_shrink_0() @@ -781,6 +782,7 @@ impl Render for TimelineView { let headers = div() .w(px(HEADER_WIDTH)) .flex_shrink_0() + .flex() .flex_col() .id("timeline-track-headers") .on_drag_move(cx.listener( @@ -863,6 +865,7 @@ impl Render for TimelineView { .id("timeline-clip-area") .relative() .overflow_hidden() + .flex() .flex_col() .on_scroll_wheel(cx.listener(|this, event: &ScrollWheelEvent, _window, cx| { if event.modifiers.control || event.modifiers.platform { @@ -1068,9 +1071,15 @@ impl Render for TimelineView { div() .absolute() .left(px(x0)) - .top(px(0.)) + // Slight vertical inset: the design's clips read + // as rounded bars with a small gap between rows, + // not full-height slabs. The corners are rounded + // in ClipElement's painted quads (content masks + // are rectangular only). + .top(px(2.)) .w(px(width)) - .h(px(clip_height)) + .h(px((clip_height - 4.0).max(1.0))) + .overflow_hidden() .id(ElementId::named_usize("timeline-clip", clip.id.0 as usize)) .on_drag( Arc::new(RwLock::new(ClipDrag { @@ -1096,6 +1105,7 @@ impl Render for TimelineView { div().size_full().flex().flex_col().child(ruler).child( div() + .flex() .flex_row() .flex_1() .relative() diff --git a/crates/gpui_widgets/src/audio_meter.rs b/crates/gpui_widgets/src/audio_meter.rs index 7276f37cc5..fd1b451223 100644 --- a/crates/gpui_widgets/src/audio_meter.rs +++ b/crates/gpui_widgets/src/audio_meter.rs @@ -6,7 +6,7 @@ use gpui::{ App, Bounds, Context, Entity, FocusHandle, Focusable, Hsla, Render, Window, canvas, - colors::DefaultColors, fill, point, prelude::*, px, size, + colors::DefaultColors, fill, hsla, point, prelude::*, px, size, }; use crate::scopes::{decay_peak, meter_lit_segments}; @@ -16,6 +16,21 @@ const SEGMENTS: usize = 16; /// Peak decay per frame (fraction of full scale). const PEAK_DECAY: f32 = 0.01; +/// Segment color by position, per the NLE convention: green for the body, +/// yellow approaching full scale, red at the top (segments count from the +/// meter's zero end). +fn segment_color(segment: usize) -> Hsla { + let yellow_from = SEGMENTS * 5 / 8; // top ~37% caution + let red_from = SEGMENTS * 13 / 16; // top ~19% clip zone + if segment >= red_from { + hsla(0.0, 0.65, 0.5, 1.0) + } else if segment >= yellow_from { + hsla(0.13, 0.75, 0.5, 1.0) + } else { + hsla(0.35, 0.6, 0.45, 1.0) + } +} + /// The orientation of an [`AudioLevelMeter`]. /// /// A horizontal meter stacks channels vertically and lights segments left to @@ -110,7 +125,6 @@ impl Render for AudioLevelMeter { move |bounds, (), window, _cx| { let width = f32::from(bounds.size.width); let height = f32::from(bounds.size.height); - let lit_color = Hsla::from(colors.selected); let dim_color = Hsla::from(colors.border); let peak_color = Hsla::from(colors.text); @@ -134,7 +148,11 @@ impl Render for AudioLevelMeter { ); window.paint_quad(fill( seg, - if segment < lit { lit_color } else { dim_color }, + if segment < lit { + segment_color(segment) + } else { + dim_color + }, )); } // Peak marker. @@ -170,7 +188,11 @@ impl Render for AudioLevelMeter { ); window.paint_quad(fill( seg, - if segment < lit { lit_color } else { dim_color }, + if segment < lit { + segment_color(segment) + } else { + dim_color + }, )); } // Peak marker. diff --git a/crates/gpui_widgets/src/project_explorer.rs b/crates/gpui_widgets/src/project_explorer.rs index 0670b187e0..d80467c8fa 100644 --- a/crates/gpui_widgets/src/project_explorer.rs +++ b/crates/gpui_widgets/src/project_explorer.rs @@ -310,6 +310,11 @@ impl Render for ProjectExplorer { } else { "" })) + // Small type glyph: a folder box for directories, a + // document for media files. The widget crate ships no + // folder/file SVGs, so the rows use text glyphs (the + // host's icon set is timeline/viewer tools only). + .child(div().w(px(16.0)).child(if entry.is_dir { "▣" } else { "▤" })) .child(div().child(entry.name.clone())); if !is_selected { row = row.hover(|style| style.bg(Hsla::from(colors.selected).opacity(0.3))); diff --git a/crates/gpui_widgets/src/viewer/mod.rs b/crates/gpui_widgets/src/viewer/mod.rs index 92d99f33e6..5ba790b613 100644 --- a/crates/gpui_widgets/src/viewer/mod.rs +++ b/crates/gpui_widgets/src/viewer/mod.rs @@ -194,6 +194,9 @@ impl Render for ViewerWidget { let mut picture = div() .id("gpui-widgets-viewer-picture") .flex_1() + .min_w_0() + .min_h_0() + .overflow_hidden() .relative() .bg(gpui::Hsla { h: 0.0, @@ -213,9 +216,12 @@ impl Render for ViewerWidget { .size_full() .object_fit(fit) .into_any(), - ViewerFrameSource::CpuFrame(image) => { - img(image.clone()).size_full().object_fit(fit).into_any() - } + ViewerFrameSource::CpuFrame(image) => img(image.clone()) + .size_full() + .min_w_0() + .min_h_0() + .object_fit(fit) + .into_any(), }; picture = picture.child(picture_element); } else { @@ -395,6 +401,8 @@ impl Render for ViewerWidget { .size_full() .flex() .flex_col() + .min_w_0() + .overflow_hidden() .child(picture) .child(transport_bar) }