Skip to content
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,18 @@ per crate.

## Unreleased

- Text input error state and selection, and title-bar close hover, now use theme colours (WI-3264, WI-3266)

- Single-line ellipsis truncation (`TruncateExt::truncate_label`) on tab labels (max width 240px token), tree rows, data table cells and headers, the title bar title and message bar/toast titles (WI-3268).

Workspace release. Includes `VideoSurface` (3.1.0), the AccessKit tree snapshot test helper and the `test-a11y-activation` vendor patch (headless test-window AccessKit activation) that rdpapp-fluent needs. Crate versions are unchanged, as in v3.1.0; consumers pin the tag.

- fluent-core, fluent-primitives, fluent-layout, fluent-ribbon, fluent-app: text no longer inherits gpui's 1.618x line height. `TextRampExt::text_ramp` sets a ramp step's size and whole-pixel line height together, `FluentApp` sets the body ramp (13/20) as the root text style, and Button, ToggleButton and dialog actions share one border-box height per size, always centre their content, and take 12/8px padding and gap from new `ComponentTokens` (WI-3256, WI-3257).
- fluent-layout: submenus open beside their parent row, flip left and snap inside the window with an 8px margin; the MenuBar dropdown anchors to the trigger's bottom-left with the same fit; MenuButton and ContextMenu click-away backdrops cover the window; popups use an 8px window margin (WI-3252, WI-3253, WI-3254).

- fluent-primitives: deny missing public API documentation and document every exported control and supporting type (WI-2945).
- fluent-primitives: switch thumb 12px with symmetric insets (40x20 track); checkbox 20px with `radii.md`; radio 20/10 (WI-3258).
- Single-line ellipsis truncation (`TruncateExt::truncate_label`) on tab labels (max width 240px token), tree rows, data table cells and headers, the title bar title and message bar/toast titles (WI-3268).

- docs: document DPI scaling conversion and resize behavior across Windows DirectX, Linux X11, and Linux Wayland, with native-run evidence requirements (WI-3061).
- Layout: add Fluent 2 Breadcrumb and Pagination controls with overflow menus, current-item semantics, RTL separators, and keyboard-ready controls (WI-3036).
Expand Down
12 changes: 9 additions & 3 deletions crates/fluent-app/src/app_builder.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use std::sync::Arc;

use fluent_core::{Theme, ThemeMode};
use fluent_core::{TextRampExt as _, Theme, ThemeMode, ThemeProvider as _};
use fluent_layout::{modal::ModalStack, ToastStack};
use gpui::{
prelude::*, px, size, App, AssetSource, Bounds, Context, Entity, IntoElement, Render,
div, prelude::*, px, size, App, AssetSource, Bounds, Context, Entity, IntoElement, Render,
SharedString, TitlebarOptions, Window, WindowBounds, WindowDecorations, WindowOptions,
};

Expand Down Expand Up @@ -55,7 +55,13 @@ impl<V: Render + 'static> Render for ThemeRoot<V> {
}
}

self.content.clone()
// Root text style: body ramp with a whole-pixel line height, so text that
// sets no line height of its own does not inherit gpui's 1.618x default.
let body = cx.theme().typography.body;
div()
.size_full()
.text_ramp(&body)
.child(self.content.clone())
}
}

Expand Down
11 changes: 6 additions & 5 deletions crates/fluent-app/src/title_bar.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use fluent_core::{LayoutDirection, Theme, ThemeProvider as _};
use fluent_core::{LayoutDirection, TextRampExt as _, Theme, ThemeProvider as _, TruncateExt as _};
use gpui::{
div, prelude::*, px, svg, ClickEvent, Context, IntoElement, MouseButton, MouseDownEvent,
Render, SharedString, Window,
Expand Down Expand Up @@ -100,7 +100,7 @@ impl Render for TitleBar {
let bar_bg = colors.surface_dim;
let fg = colors.on_neutral;
let ctrl_hover = colors.subtle_hover;
let close_hover: gpui::Hsla = gpui::rgb(0xC42B1C).into();
let close_hover = colors.status_error;

// Drag: left-button press starts window move; consecutive presses
// within DOUBLE_CLICK_WINDOW_MS toggle maximize / restore instead.
Expand Down Expand Up @@ -139,23 +139,24 @@ impl Render for TitleBar {
let accent_fg = colors.on_neutral_accent;
let mut title_area = div()
.flex_1()
.min_w_0()
.h_full()
.flex()
.items_center()
.gap(px(spacing.sm))
.when(!rtl, |area| area.pl(px(spacing.md)))
.when(rtl, |area| area.pr(px(spacing.md)))
.text_size(px(typography.caption.size))
.text_ramp(&typography.caption)
.text_color(fg)
.on_mouse_down(MouseButton::Left, drag_handler);
if let Some(path) = icon {
title_area = title_area.child(svg().path(path).size(px(16.0)).text_color(accent_fg));
}
let mut title_area = title_area.child(title);
let mut title_area = title_area.child(div().truncate_label().child(title));
if let Some(status) = status {
title_area = title_area.child(
div()
.text_size(px(typography.caption.size))
.text_ramp(&typography.caption)
.text_color(colors.on_subtle)
.child(status),
);
Expand Down
19 changes: 19 additions & 0 deletions crates/fluent-core/src/components.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,17 @@ pub struct ComponentTokens {
pub button_medium_icon_size: f32,
/// Public API item.
pub button_large_icon_size: f32,
/// Fluent 2 button horizontal padding for small, medium, and large controls.
/// Padding sits inside the control's border-box height.
pub button_small_padding_x: f32,
/// Public API item.
pub button_medium_padding_x: f32,
/// Public API item.
pub button_large_padding_x: f32,
/// Fluent 2 gap between a button's icon and label (small, then medium/large).
pub button_small_gap: f32,
/// Public API item.
pub button_medium_gap: f32,
/// Public API item.
pub menu_bar_height: f32,
/// Public API item.
Expand Down Expand Up @@ -65,6 +76,8 @@ pub struct ComponentTokens {
pub content_tab_icon_size: f32,
/// Public API item.
pub content_tab_close_size: f32,
/// Maximum width of a content tab before its label is ellipsised.
pub content_tab_max_width: f32,
/// Public API item.
pub ribbon_tab_height: f32,
/// Public API item.
Expand All @@ -88,6 +101,11 @@ impl Default for ComponentTokens {
button_small_icon_size: 12.0,
button_medium_icon_size: 16.0,
button_large_icon_size: 20.0,
button_small_padding_x: 8.0,
button_medium_padding_x: 12.0,
button_large_padding_x: 16.0,
button_small_gap: 4.0,
button_medium_gap: 8.0,
menu_bar_height: 28.0,
menu_item_height: 28.0,
menu_min_width: 200.0,
Expand All @@ -112,6 +130,7 @@ impl Default for ComponentTokens {
content_tab_strip_height: 32.0,
content_tab_icon_size: 14.0,
content_tab_close_size: 14.0,
content_tab_max_width: 240.0,
ribbon_tab_height: 28.0,
ribbon_content_button_height: 64.0,
ribbon_group_label_height: 16.0,
Expand Down
4 changes: 3 additions & 1 deletion crates/fluent-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ mod shadows;
mod spacing;
mod stroke_width;
mod theme;
mod truncate;
mod typography;

/// Re-export the underlying UI toolkit for examples and downstream Fluent crates.
Expand All @@ -29,7 +30,8 @@ pub use stroke_width::StrokeWidthTokens;
pub use theme::{
Brightness, LayoutDirection, LayoutDirectionProvider, Theme, ThemeMode, ThemeProvider,
};
pub use typography::{TypeRamp, TypographyTokens};
pub use truncate::TruncateExt;
pub use typography::{TextRampExt, TypeRamp, TypographyTokens};

#[cfg(test)]
mod tests {
Expand Down
16 changes: 16 additions & 0 deletions crates/fluent-core/src/truncate.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
use gpui::Styled;

/// Single-line truncation with a trailing ellipsis.
pub trait TruncateExt: Styled + Sized {
/// Keeps the element on one line, clips overflow and ends it with an
/// ellipsis. Also sets `min-width: 0` so a flex item can shrink below its
/// content width.
fn truncate_label(self) -> Self {
self.min_w_0()
.overflow_hidden()
.whitespace_nowrap()
.text_ellipsis()
}
}

impl<T: Styled + Sized> TruncateExt for T {}
26 changes: 26 additions & 0 deletions crates/fluent-core/src/typography.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use gpui::{px, Styled};

/// A single typographic ramp step.
///
/// `size` and `line_height` are in logical pixels (DIPs).
Expand Down Expand Up @@ -68,3 +70,27 @@ impl Default for TypographyTokens {
}
}
}

impl TypeRamp {
/// Font size as a whole-pixel length.
pub fn font_size_px(&self) -> gpui::Pixels {
px(self.size.round())
}

/// Line height as a whole-pixel length, so line boxes never land on half pixels.
pub fn line_height_px(&self) -> gpui::Pixels {
px(self.line_height.round())
}
}

/// Applies a [`TypeRamp`] step's size and whole-pixel line height together, so a
/// text size can never be set without its line height (gpui's default is 1.618x).
pub trait TextRampExt: Styled + Sized {
/// Sets `text_size` and `line_height` from `ramp`.
fn text_ramp(self, ramp: &TypeRamp) -> Self {
self.text_size(ramp.font_size_px())
.line_height(ramp.line_height_px())
}
}

impl<T: Styled> TextRampExt for T {}
35 changes: 27 additions & 8 deletions crates/fluent-layout/src/command_palette.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use fluent_core::TextRampExt;
use std::{fmt, rc::Rc};

use fluent_core::{Theme, ThemeProvider as _};
Expand Down Expand Up @@ -148,6 +149,7 @@ pub struct CommandPalette {
entries: Vec<PaletteEntry>,
filtered: Vec<usize>,
selected: usize,
results_scroll: gpui::ScrollHandle,
max_results: Option<usize>,
/// Public API item.
pub on_select: Option<Box<dyn Fn(&PaletteEntry, &mut App) + 'static>>,
Expand All @@ -164,6 +166,7 @@ impl CommandPalette {
entries: vec![],
filtered: vec![],
selected: 0,
results_scroll: gpui::ScrollHandle::new(),
max_results: None,
on_select: None,
}
Expand Down Expand Up @@ -485,7 +488,15 @@ impl Render for CommandPalette {
let entries = &self.entries;

// Build result rows
let mut results = div().flex().flex_col().max_h(px(360.0)).overflow_hidden();
let mut results = div()
.id("command-palette-results")
.flex()
.flex_col()
.max_h(px(360.0))
.overflow_y_scroll()
.track_scroll(&self.results_scroll);
let mut child_count = 0usize;
let mut selected_child = None;

let mut previous_group: Option<SharedString> = None;
for (row_idx, &entry_idx) in filtered.iter().enumerate() {
Expand All @@ -502,12 +513,13 @@ impl Render for CommandPalette {

if group.is_some() && group != previous_group {
if let Some(group_label) = group.clone() {
child_count += 1;
results = results.child(
div()
.px(px(spacing.md))
.pt(px(spacing.sm))
.pb(px(4.0))
.text_size(px(typography.caption.size))
.text_ramp(&typography.caption)
.font_weight(FontWeight::SEMIBOLD)
.text_color(colors.on_subtle)
.child(group_label),
Expand Down Expand Up @@ -571,7 +583,7 @@ impl Render for CommandPalette {

let mut text_col = div().flex().flex_col().flex_1().child(
div()
.text_size(px(typography.body.size))
.text_ramp(&typography.body)
.text_color(label_fg)
.when(is_selected, |d| d.font_weight(FontWeight::SEMIBOLD))
.child(label),
Expand All @@ -580,7 +592,7 @@ impl Render for CommandPalette {
if let Some(sub) = subtitle {
text_col = text_col.child(
div()
.text_size(px(typography.caption.size))
.text_ramp(&typography.caption)
.text_color(sub_fg)
.child(sub),
);
Expand All @@ -595,13 +607,20 @@ impl Render for CommandPalette {
.rounded(px(radii.sm))
.border_1()
.border_color(colors.stroke_neutral_subtle)
.text_size(px(typography.caption.size))
.text_ramp(&typography.caption)
.text_color(sub_fg)
.child(hint),
);
}
if is_selected {
selected_child = Some(child_count);
}
child_count += 1;
results = results.child(row_el);
}
if let Some(ix) = selected_child {
self.results_scroll.scroll_to_item(ix);
}

// Empty state
if filtered.is_empty() {
Expand All @@ -612,7 +631,7 @@ impl Render for CommandPalette {
.flex()
.items_center()
.justify_center()
.text_size(px(typography.body.size))
.text_ramp(&typography.body)
.text_color(colors.on_subtle_disabled)
.child("No results"),
);
Expand Down Expand Up @@ -647,7 +666,7 @@ impl Render for CommandPalette {
.child(
div()
.flex_1()
.text_size(px(typography.body.size))
.text_ramp(&typography.body)
.text_color(input_text_color)
.child(query_display),
),
Expand All @@ -667,7 +686,7 @@ impl Render for CommandPalette {
.border_color(colors.stroke_neutral_subtle)
.child(
div()
.text_size(px(typography.caption.size))
.text_ramp(&typography.caption)
.text_color(colors.on_subtle_disabled)
.child("↑↓ navigate · ↵ select · Esc close"),
),
Expand Down
Loading
Loading