style: professional dark theme refresh aligned with the design spec
- oak theme palette to a denser professional dark (near-black base, container/window surfaces, subtler secondary text). - Dock tabs: selected tab solid accent with white text, hairline separators between panels. - Timeline: green clip blocks, blue playhead, neutral track headers, brighter ruler labels, deep-green waveforms. - Viewer: green timecode readout, solid-accent play button, chip-style fit/safe-margin buttons. - Effect stack cards and node cards get rounded borders; spinbox values in amber per the design.
This commit is contained in:
@@ -9,7 +9,8 @@
|
||||
use crate::colors::DefaultColors;
|
||||
use crate::{
|
||||
App, AppContext, Axis, ClickEvent, Context, ElementId, EventEmitter, InteractiveElement,
|
||||
IntoElement, Pixels, Point, Render, StatefulInteractiveElement, Styled, Window, div, px,
|
||||
IntoElement, ParentElement, Pixels, Point, Render, StatefulInteractiveElement, Styled, Window,
|
||||
div, px,
|
||||
};
|
||||
|
||||
use super::{NodePath, path_key};
|
||||
@@ -194,13 +195,21 @@ impl Render for SplitHandle {
|
||||
cx.new(|_cx| SplitDragGhost { direction })
|
||||
};
|
||||
|
||||
// The hitbox stays HITBOX-wide for grabbability, but the visible
|
||||
// divider is a centered 1px hairline — the design separates panels
|
||||
// with thin lines, not thick bars.
|
||||
let line = div()
|
||||
.flex_none()
|
||||
.bg(colors.separator);
|
||||
let mut root = div()
|
||||
.id(ElementId::named_usize(
|
||||
"dock-split-handle",
|
||||
path_key(&self.path),
|
||||
))
|
||||
.flex_none()
|
||||
.bg(colors.separator)
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.on_click(cx.listener(move |this, event: &ClickEvent, _window, cx| {
|
||||
if event.click_count() >= 2 {
|
||||
this.reset(cx);
|
||||
@@ -209,14 +218,18 @@ impl Render for SplitHandle {
|
||||
|
||||
// A horizontal split stacks children side by side, so its divider is
|
||||
// a vertical bar and vice versa.
|
||||
match direction {
|
||||
Axis::Horizontal => {
|
||||
root = root.w(px(Self::HITBOX.0)).h_full().cursor_col_resize();
|
||||
}
|
||||
Axis::Vertical => {
|
||||
root = root.w_full().h(px(Self::HITBOX.0)).cursor_row_resize();
|
||||
}
|
||||
}
|
||||
root = match direction {
|
||||
Axis::Horizontal => root
|
||||
.w(px(Self::HITBOX.0))
|
||||
.h_full()
|
||||
.cursor_col_resize()
|
||||
.child(line.w(px(1.0)).h_full()),
|
||||
Axis::Vertical => root
|
||||
.w_full()
|
||||
.h(px(Self::HITBOX.0))
|
||||
.cursor_row_resize()
|
||||
.child(line.w_full().h(px(1.0))),
|
||||
};
|
||||
|
||||
root.on_drag(
|
||||
SplitHandleDrag {
|
||||
|
||||
@@ -244,6 +244,11 @@ impl Render for TabBar {
|
||||
.h(px(26.0))
|
||||
.w_full()
|
||||
.overflow_hidden()
|
||||
// The strip reads as raised chrome above the panel content, closed
|
||||
// off by a hairline border (per the design's dense panel headers).
|
||||
.bg(colors.container)
|
||||
.border_b_1()
|
||||
.border_color(colors.border)
|
||||
.on_scroll_wheel(cx.listener(|this, event: &ScrollWheelEvent, _window, cx| {
|
||||
let delta = match event.delta {
|
||||
ScrollDelta::Pixels(delta) => delta.x.0,
|
||||
@@ -278,6 +283,9 @@ impl Render for TabBar {
|
||||
|
||||
for (index, &panel) in self.tabs.iter().enumerate() {
|
||||
let active = index == self.active;
|
||||
// The hover fill for inactive tabs, hoisted out of the closure so
|
||||
// the shared `colors` isn't moved (it is behind an `Arc`).
|
||||
let inactive_hover = colors.background;
|
||||
let title = self
|
||||
.titles
|
||||
.get(index)
|
||||
@@ -310,12 +318,26 @@ impl Render for TabBar {
|
||||
.whitespace_nowrap()
|
||||
.cursor_pointer()
|
||||
.text_sm()
|
||||
// The active tab is the accent-filled chip of the design;
|
||||
// inactive tabs sit flat on the strip with dimmed labels and
|
||||
// surface on hover.
|
||||
.bg(if active {
|
||||
colors.selected
|
||||
} else {
|
||||
colors.background
|
||||
colors.container
|
||||
})
|
||||
.text_color(if active {
|
||||
colors.selected_text
|
||||
} else {
|
||||
colors.disabled
|
||||
})
|
||||
.hover(move |style| {
|
||||
if active {
|
||||
style
|
||||
} else {
|
||||
style.bg(inactive_hover)
|
||||
}
|
||||
})
|
||||
.text_color(if active { colors.text } else { colors.disabled })
|
||||
.on_click(cx.listener(move |this, _event: &ClickEvent, window, cx| {
|
||||
this.activate(index, window, cx);
|
||||
}))
|
||||
|
||||
@@ -439,7 +439,15 @@ impl<D: EffectStackDataSource> Render for EffectStackView<D> {
|
||||
.child(label),
|
||||
);
|
||||
|
||||
let mut column = div().id("effect-stack-cards").flex().flex_col().w_full();
|
||||
// Cards breathe as separate rounded panels (the design's stack), not
|
||||
// as a contiguous list.
|
||||
let mut column = div()
|
||||
.id("effect-stack-cards")
|
||||
.flex()
|
||||
.flex_col()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.p_2();
|
||||
|
||||
for (index, effect) in effects.iter().enumerate() {
|
||||
let id = effect.id();
|
||||
@@ -458,7 +466,9 @@ impl<D: EffectStackDataSource> Render for EffectStackView<D> {
|
||||
}
|
||||
|
||||
// Header row: drag handle, enable toggle, the card itself
|
||||
// (flexing to fill), and the remove button.
|
||||
// (flexing to fill), and the remove button. Effect cards get the
|
||||
// design's two-tone treatment: a raised header strip over the
|
||||
// darker card body.
|
||||
let mut header_row = div()
|
||||
.id(ElementId::named_usize("effect-header", id.0 as usize))
|
||||
.flex()
|
||||
@@ -471,6 +481,9 @@ impl<D: EffectStackDataSource> Render for EffectStackView<D> {
|
||||
cx.stop_propagation();
|
||||
this.context_menu(id, event.position(), cx);
|
||||
}));
|
||||
if !fixed {
|
||||
header_row = header_row.bg(colors.container);
|
||||
}
|
||||
|
||||
if !fixed {
|
||||
header_row = header_row.cursor_pointer().on_click(cx.listener(
|
||||
@@ -545,15 +558,17 @@ impl<D: EffectStackDataSource> Render for EffectStackView<D> {
|
||||
.border_1()
|
||||
.border_color(if selected_effect == Some(id) {
|
||||
colors.selected
|
||||
} else if fixed {
|
||||
} else {
|
||||
colors.border
|
||||
} else {
|
||||
colors.separator
|
||||
})
|
||||
.bg(if fixed {
|
||||
colors.container
|
||||
} else {
|
||||
colors.background
|
||||
// The source card is the design's accent-tinted media bar; the
|
||||
// output card and effect bodies stay neutral.
|
||||
.bg(match effect.kind() {
|
||||
EffectCardKind::Source => {
|
||||
crate::Background::from(crate::Hsla::from(colors.selected).opacity(0.3))
|
||||
}
|
||||
EffectCardKind::Output => crate::Background::from(colors.container),
|
||||
EffectCardKind::Effect => crate::Background::from(colors.background),
|
||||
})
|
||||
.overflow_hidden();
|
||||
wrapper = wrapper.child(header_row);
|
||||
|
||||
@@ -256,9 +256,10 @@ impl NodeElement {
|
||||
window.paint_quad(fill(bounds, colors.background));
|
||||
|
||||
// Border quad: transparent fill, themed border (accent when selected).
|
||||
// The 6px corner matches the design's softly rounded node cards.
|
||||
window.paint_quad(PaintQuad {
|
||||
bounds,
|
||||
corner_radii: Corners::all(px(4.0) * zoom),
|
||||
corner_radii: Corners::all(px(6.0) * zoom),
|
||||
background: hsla(0.0, 0.0, 0.0, 0.0).into(),
|
||||
border_widths: Edges::all(if self.visual.selected {
|
||||
px(1.5)
|
||||
|
||||
@@ -240,9 +240,11 @@ impl RenderOnce for TimelineRuler {
|
||||
},
|
||||
move |bounds, content, window, cx| {
|
||||
let baseline_color = hsla(0.0, 0.0, 0.5, 0.5);
|
||||
let major_color = hsla(0.0, 0.0, 0.6, 0.9);
|
||||
let minor_color = hsla(0.0, 0.0, 0.6, 0.45);
|
||||
let text_color = hsla(0.0, 0.0, 0.5, 1.0);
|
||||
let major_color = hsla(0.0, 0.0, 0.55, 0.9);
|
||||
let minor_color = hsla(0.0, 0.0, 0.55, 0.4);
|
||||
// Ruler labels read as the design's muted light gray against
|
||||
// the near-black ruler strip.
|
||||
let text_color = hsla(0.0, 0.0, 0.62, 1.0);
|
||||
let band_color = hsla(0.63, 0.55, 0.55, 0.10);
|
||||
let bottom = bounds.bottom();
|
||||
|
||||
|
||||
@@ -1655,9 +1655,9 @@ fn drag_ghost<T>(
|
||||
cx.new(|_cx| DragPreview)
|
||||
}
|
||||
|
||||
/// The playhead line color.
|
||||
/// The playhead line color: the design's accent-blue vertical line.
|
||||
fn playhead_color() -> Hsla {
|
||||
hsla(0.0, 0.0, 0.9, 0.9)
|
||||
hsla(0.60, 0.90, 0.60, 1.0)
|
||||
}
|
||||
|
||||
/// Reshapes the work-area band for a ruler drag move.
|
||||
@@ -1930,10 +1930,13 @@ mod tests {
|
||||
}
|
||||
|
||||
/// The default body color for clips on a track of `kind`.
|
||||
///
|
||||
/// Per the design both media kinds read as green bars (the audio waveform
|
||||
/// supplies the contrast); subtitles keep the amber family.
|
||||
fn kind_color(kind: TrackKind) -> Hsla {
|
||||
match kind {
|
||||
TrackKind::Video => hsla(0.58, 0.45, 0.35, 1.0),
|
||||
TrackKind::Audio => hsla(0.35, 0.45, 0.35, 1.0),
|
||||
TrackKind::Video => hsla(0.40, 0.44, 0.43, 1.0),
|
||||
TrackKind::Audio => hsla(0.38, 0.45, 0.36, 1.0),
|
||||
TrackKind::Subtitle => hsla(0.10, 0.45, 0.35, 1.0),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,10 @@
|
||||
//! `TimelineEvent::TrackToggleRequested`); without a handler they render as
|
||||
//! inert status glyphs.
|
||||
|
||||
use crate::{App, ClickEvent, ElementId, Hsla, SharedString, Window, div, hsla, prelude::*, px};
|
||||
use crate::{
|
||||
App, ClickEvent, ElementId, SharedString, Window, colors::DefaultColors, div, hsla, prelude::*,
|
||||
px,
|
||||
};
|
||||
|
||||
use super::data::TrackKind;
|
||||
|
||||
@@ -123,24 +126,6 @@ impl TrackHeader {
|
||||
self.index
|
||||
}
|
||||
|
||||
/// The background tint for a track of `kind`.
|
||||
fn kind_background(kind: TrackKind) -> Hsla {
|
||||
match kind {
|
||||
TrackKind::Video => hsla(0.58, 0.45, 0.32, 0.18),
|
||||
TrackKind::Audio => hsla(0.35, 0.45, 0.32, 0.18),
|
||||
TrackKind::Subtitle => hsla(0.10, 0.45, 0.32, 0.18),
|
||||
}
|
||||
}
|
||||
|
||||
/// The label color for a track of `kind`.
|
||||
fn kind_text(kind: TrackKind) -> Hsla {
|
||||
match kind {
|
||||
TrackKind::Video => hsla(0.58, 0.35, 0.85, 1.0),
|
||||
TrackKind::Audio => hsla(0.35, 0.35, 0.85, 1.0),
|
||||
TrackKind::Subtitle => hsla(0.10, 0.35, 0.85, 1.0),
|
||||
}
|
||||
}
|
||||
|
||||
/// A small toggle glyph (one or two letters) reflecting `active`. When an
|
||||
/// [`Self::on_toggle`] handler is installed the glyph is a click target
|
||||
/// emitting `event`; the click stops propagating so it never toggles the
|
||||
@@ -205,14 +190,16 @@ impl TrackHeader {
|
||||
}
|
||||
|
||||
impl RenderOnce for TrackHeader {
|
||||
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||
let background = Self::kind_background(self.kind);
|
||||
let text = Self::kind_text(self.kind);
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
// Neutral chrome per the design: the header cell sits on the panel
|
||||
// container color with primary-text labels; the track kind is conveyed
|
||||
// by the toggle glyphs, not by a tinted background.
|
||||
let colors = cx.default_colors().clone();
|
||||
let separator_height = px(TrackHeader::SEPARATOR_HEIGHT);
|
||||
|
||||
div()
|
||||
.size_full()
|
||||
.bg(background)
|
||||
.bg(colors.container)
|
||||
.flex()
|
||||
.flex_col()
|
||||
.child(
|
||||
@@ -223,7 +210,12 @@ impl RenderOnce for TrackHeader {
|
||||
.items_center()
|
||||
.gap(px(6.))
|
||||
.px_2()
|
||||
.child(div().text_sm().text_color(text).child(self.name.clone()))
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(colors.text)
|
||||
.child(self.name.clone()),
|
||||
)
|
||||
.child(div().flex_1())
|
||||
.child(self.toggle_row()),
|
||||
)
|
||||
|
||||
@@ -173,6 +173,9 @@ impl Render for SpinBox {
|
||||
.border_1()
|
||||
.border_color(colors.border)
|
||||
.bg(colors.background)
|
||||
// Editable values render in the theme's amber (the
|
||||
// design's gold numerals), distinct from labels.
|
||||
.text_color(crate::theme::current_theme(cx).link)
|
||||
.px_1()
|
||||
.flex()
|
||||
.items_center()
|
||||
|
||||
@@ -45,18 +45,22 @@ fn rgba(hex: u32) -> Rgba {
|
||||
|
||||
impl OakTheme {
|
||||
/// The olive-dark palette (oak's default).
|
||||
///
|
||||
/// Tuned to the design's professional dark scheme: a near-black content
|
||||
/// base, slightly raised panel containers, a low-contrast separator and a
|
||||
/// dimmed secondary text — the blue accent stays the single saturated hue.
|
||||
pub fn olive_dark() -> Self {
|
||||
Self {
|
||||
name: "Olive Dark".into(),
|
||||
window: rgba(0x353535),
|
||||
base: rgba(0x191919),
|
||||
alternate_base: rgba(0x353535),
|
||||
text: rgb(0xffffff),
|
||||
window: rgba(0x23272D),
|
||||
base: rgba(0x16181D),
|
||||
alternate_base: rgba(0x2E333B),
|
||||
text: rgb(0xE8EAED),
|
||||
accent: rgba(0x2A82DA),
|
||||
accent_text: rgb(0xffffff),
|
||||
link: rgba(0xE0B040),
|
||||
disabled_text: rgba(0xA0A0A0),
|
||||
disabled_button_text: rgba(0x808080),
|
||||
disabled_text: rgba(0x7D838C),
|
||||
disabled_button_text: rgba(0x565C66),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -414,11 +414,15 @@ impl<C: PlaybackClock> Render for ViewerWidget<C> {
|
||||
.px_2()
|
||||
.py_0p5()
|
||||
.bg(colors.container)
|
||||
.border_t_1()
|
||||
.border_color(colors.border)
|
||||
.child(transport_button(
|
||||
"gpui-widgets-viewer-in",
|
||||
in_icon,
|
||||
"⏮",
|
||||
crate::i18n::tr("viewer.in_point", "入点"),
|
||||
false,
|
||||
&colors,
|
||||
cx.listener(|this, _event: &ClickEvent, _window, cx| {
|
||||
this.emit(
|
||||
ViewerEvent::InPointRequested {
|
||||
@@ -433,6 +437,8 @@ impl<C: PlaybackClock> Render for ViewerWidget<C> {
|
||||
step_back_icon,
|
||||
"⏪",
|
||||
crate::i18n::tr("viewer.step_back", "上一帧"),
|
||||
false,
|
||||
&colors,
|
||||
cx.listener(|this, _event: &ClickEvent, _window, cx| {
|
||||
this.emit(
|
||||
ViewerEvent::StepRequested {
|
||||
@@ -452,6 +458,8 @@ impl<C: PlaybackClock> Render for ViewerWidget<C> {
|
||||
} else {
|
||||
crate::i18n::tr("viewer.play", "播放")
|
||||
},
|
||||
true,
|
||||
&colors,
|
||||
cx.listener(|this, _event: &ClickEvent, _window, cx| {
|
||||
let event = if this.transport.playing {
|
||||
ViewerEvent::PauseRequested {
|
||||
@@ -470,6 +478,8 @@ impl<C: PlaybackClock> Render for ViewerWidget<C> {
|
||||
step_forward_icon,
|
||||
"⏩",
|
||||
crate::i18n::tr("viewer.step_forward", "下一帧"),
|
||||
false,
|
||||
&colors,
|
||||
cx.listener(|this, _event: &ClickEvent, _window, cx| {
|
||||
this.emit(
|
||||
ViewerEvent::StepRequested {
|
||||
@@ -485,6 +495,8 @@ impl<C: PlaybackClock> Render for ViewerWidget<C> {
|
||||
out_icon,
|
||||
"⏭",
|
||||
crate::i18n::tr("viewer.out_point", "出点"),
|
||||
false,
|
||||
&colors,
|
||||
cx.listener(|this, _event: &ClickEvent, _window, cx| {
|
||||
this.emit(
|
||||
ViewerEvent::OutPointRequested {
|
||||
@@ -499,6 +511,8 @@ impl<C: PlaybackClock> Render for ViewerWidget<C> {
|
||||
None,
|
||||
x_glyph(colors.text),
|
||||
crate::i18n::tr("viewer.clear_range", "清除入出点"),
|
||||
false,
|
||||
&colors,
|
||||
cx.listener(|this, _event: &ClickEvent, _window, cx| {
|
||||
this.emit(
|
||||
ViewerEvent::ClearRangeRequested {
|
||||
@@ -508,11 +522,19 @@ impl<C: PlaybackClock> Render for ViewerWidget<C> {
|
||||
);
|
||||
}),
|
||||
))
|
||||
.child(div().px_2().text_xs().text_color(colors.text).child(timecode))
|
||||
// The current timecode reads in the design's bright green.
|
||||
.child(
|
||||
div()
|
||||
.px_2()
|
||||
.text_xs()
|
||||
.text_color(gpui::hsla(0.33, 0.75, 0.62, 1.0))
|
||||
.child(timecode),
|
||||
)
|
||||
.child(div().flex_1())
|
||||
.child(button(
|
||||
"gpui-widgets-viewer-safe",
|
||||
crate::i18n::tr("viewer.safe_frames", "安全框"),
|
||||
&colors,
|
||||
cx.listener(|this, _event: &ClickEvent, _window, cx| {
|
||||
this.show_safe_frames = !this.show_safe_frames;
|
||||
this.emit(
|
||||
@@ -526,6 +548,7 @@ impl<C: PlaybackClock> Render for ViewerWidget<C> {
|
||||
.child(button(
|
||||
"gpui-widgets-viewer-zoom",
|
||||
crate::i18n::tr("viewer.zoom", "缩放"),
|
||||
&colors,
|
||||
cx.listener(|this, _event: &ClickEvent, _window, cx| {
|
||||
this.zoom = !this.zoom;
|
||||
this.emit(
|
||||
@@ -550,14 +573,20 @@ impl<C: PlaybackClock> Render for ViewerWidget<C> {
|
||||
|
||||
/// A transport icon button: a 16px icon on a 24px hit target with a
|
||||
/// localized tooltip. Falls back to the `fallback` glyph when `icon` is
|
||||
/// `None` (no resolver registered, or no file for the name).
|
||||
/// `None` (no resolver registered, or no file for the name). `primary` marks
|
||||
/// the design's accent-filled play/pause button; the rest are flat buttons
|
||||
/// that surface on hover.
|
||||
fn transport_button(
|
||||
id: &'static str,
|
||||
icon: Option<std::path::PathBuf>,
|
||||
fallback: impl IntoElement,
|
||||
tooltip: SharedString,
|
||||
primary: bool,
|
||||
colors: &gpui::colors::Colors,
|
||||
on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||
) -> impl IntoElement {
|
||||
let background = colors.selected;
|
||||
let hover = colors.container;
|
||||
let mut el = div()
|
||||
.id(id)
|
||||
.debug_selector(move || id.into())
|
||||
@@ -568,7 +597,19 @@ fn transport_button(
|
||||
.justify_center()
|
||||
.rounded_md()
|
||||
.cursor_pointer()
|
||||
.hover(|style| style.bg(gpui::colors::Colors::dark().selected))
|
||||
.text_color(if primary {
|
||||
colors.selected_text
|
||||
} else {
|
||||
colors.text
|
||||
})
|
||||
.when(primary, |style| style.bg(background))
|
||||
.hover(move |style| {
|
||||
if primary {
|
||||
style.bg(background)
|
||||
} else {
|
||||
style.bg(hover)
|
||||
}
|
||||
})
|
||||
.tooltip(move |window, cx| tooltip_view(tooltip.clone(), window, cx))
|
||||
.on_click(on_click);
|
||||
if let Some(path) = icon {
|
||||
@@ -609,20 +650,27 @@ fn x_glyph(color: gpui::Rgba) -> impl IntoElement {
|
||||
)
|
||||
}
|
||||
|
||||
/// A small labeled button.
|
||||
/// A small labeled button, styled as the design's bordered chip (the "适合 /
|
||||
/// 安全框" controls at the transport bar's right end).
|
||||
fn button(
|
||||
id: &'static str,
|
||||
label: impl IntoElement,
|
||||
colors: &gpui::colors::Colors,
|
||||
on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||
) -> impl IntoElement {
|
||||
let border = colors.border;
|
||||
let hover = colors.separator;
|
||||
div()
|
||||
.id(id)
|
||||
.debug_selector(move || id.into())
|
||||
.px_2()
|
||||
.py_0p5()
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.border_color(border)
|
||||
.text_color(colors.text)
|
||||
.cursor_pointer()
|
||||
.hover(|style| style.bg(gpui::colors::Colors::dark().selected))
|
||||
.hover(move |style| style.bg(hover))
|
||||
.on_click(on_click)
|
||||
.child(label)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user