Move window controls into the title bar

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Georg Bauer
2026-07-25 23:35:57 +02:00
parent ae4222a573
commit 7f35d7f58d
8 changed files with 99 additions and 22 deletions

View File

@@ -19,7 +19,7 @@ use iced::widget::{
Button, Space, Svg, button, checkbox, column, container, horizontal_rule, markdown, opaque,
pick_list, progress_bar, row, scrollable, stack, svg, text, text_input,
};
use iced::{Alignment, Background, Border, Color, Element, Length, Theme, window};
use iced::{Alignment, Background, Border, Color, Element, Length, Padding, Theme, window};
use std::collections::VecDeque;
use std::path::Path;
@@ -35,8 +35,18 @@ const ICON_SEND: &[u8] = include_bytes!("../../assets/icons/send.svg");
const ICON_MODEL: &[u8] = include_bytes!("../../assets/icons/model.svg");
const ICON_SPARK: &[u8] = include_bytes!("../../assets/icons/spark.svg");
const ICON_PIN: &[u8] = include_bytes!("../../assets/icons/pin.svg");
const ICON_SIDEBAR: &[u8] = include_bytes!("../../assets/icons/sidebar.svg");
const ICON_ARCHIVE: &[u8] = include_bytes!("../../assets/icons/archive.svg");
/// Height of the strip the window content shares with the native title bar.
/// Keep it close to the 28pt macOS title bar so our controls line up with the
/// traffic lights.
const TITLE_BAR_HEIGHT: f32 = 30.0;
/// Height of every control in that strip.
const TITLE_BAR_CONTROL: f32 = 24.0;
/// Space the macOS traffic lights need before our own controls start.
const TRAFFIC_LIGHT_WIDTH: f32 = 78.0;
impl App {
pub(crate) fn view(&self, id: window::Id) -> Element<'_, Message> {
if self.model_manager_window == Some(id) {
@@ -52,11 +62,22 @@ impl App {
}
fn main_view(&self) -> Element<'_, Message> {
let mut shell = column![
row![self.sidebar(), self.detail()]
let mut body = row![].width(Length::Fill).height(Length::Fill);
if !self.preferences.sidebar_collapsed {
body = body.push(self.sidebar());
}
body = body.push(
container(self.detail())
.width(Length::Fill)
.height(Length::Fill)
];
.padding(Padding::ZERO.top(TITLE_BAR_HEIGHT)),
);
// The title bar floats above the panels so their backgrounds run all the
// way to the top edge; the spacer below it lets clicks through.
let mut shell = column![stack![
body,
column![self.title_bar(), Space::with_height(Length::Fill)],
]];
if let ModelDownload::Active(download) = &self.model_download {
shell = shell.push(download_status_bar(download));
}
@@ -232,7 +253,7 @@ impl App {
)
.width(276)
.height(Length::Fill)
.padding(16)
.padding(Padding::new(16.0).top(16.0 + TITLE_BAR_HEIGHT))
.style(sidebar_style)
.into()
}
@@ -273,19 +294,39 @@ impl App {
.into()
}
fn detail(&self) -> Element<'_, Message> {
/// Content that shares the strip with the native traffic lights.
fn title_bar(&self) -> Element<'_, Message> {
let toggle = button(icon(ICON_SIDEBAR, 16))
.height(TITLE_BAR_CONTROL)
.padding([0, 6])
.on_press(Message::ToggleSidebar)
.style(button::text);
stack![
container(self.detail_tabs())
.center_x(Length::Fill)
.center_y(Length::Fill),
container(row![Space::with_width(TRAFFIC_LIGHT_WIDTH), toggle]).center_y(Length::Fill),
]
.width(Length::Fill)
.height(TITLE_BAR_HEIGHT)
.into()
}
fn detail_tabs(&self) -> Element<'_, Message> {
let chat_active = self.detail_tab == DetailTab::Chat;
let stats_active = self.detail_tab == DetailTab::Stats;
let tabs = container(
row![
button(text("Chat").size(13))
.width(92)
.padding([7, 18])
.width(88)
.height(TITLE_BAR_CONTROL - 4.0)
.padding([0, 16])
.on_press(Message::ShowChat)
.style(move |theme, status| segmented_button_style(theme, status, chat_active)),
button(text("Stats").size(13))
.width(92)
.padding([7, 18])
.width(88)
.height(TITLE_BAR_CONTROL - 4.0)
.padding([0, 16])
.on_press(Message::ShowStats)
.style(move |theme, status| segmented_button_style(
theme,
@@ -295,19 +336,16 @@ impl App {
]
.spacing(2),
)
.padding(3)
.padding(2)
.style(segmented_control_style);
let body = match self.detail_tab {
tabs.into()
}
fn detail(&self) -> Element<'_, Message> {
match self.detail_tab {
DetailTab::Chat => self.chat_detail(),
DetailTab::Stats => self.stats_dashboard(),
};
column![
container(tabs).center_x(Length::Fill).padding([12, 0]),
body
]
.width(Length::Fill)
.height(Length::Fill)
.into()
}
}
fn project_dialog<'a>(&'a self, path: &'a Path) -> Element<'a, Message> {
@@ -664,7 +702,7 @@ fn segmented_control_style(_: &Theme) -> container::Style {
border: Border {
color: Color::from_rgb8(57, 57, 60),
width: 1.0,
radius: 18.0.into(),
radius: (TITLE_BAR_CONTROL / 2.0).into(),
},
..container::Style::default()
}
@@ -682,7 +720,7 @@ fn segmented_button_style(_: &Theme, status: button::Status, selected: bool) ->
background,
text_color: if selected { Color::WHITE } else { muted_text() },
border: Border {
radius: 14.0.into(),
radius: (TITLE_BAR_CONTROL / 2.0 - 2.0).into(),
..Border::default()
},
..button::Style::default()