feat: more work on UI

This commit is contained in:
2026-04-04 19:23:12 +02:00
parent eedd0a9118
commit 03dcaea88e
25 changed files with 713 additions and 178 deletions

View File

@@ -1,24 +1,27 @@
use iced::widget::{button, column, container, text, Column, Space};
use iced::{Alignment, Element, Length};
use iced::widget::{button, column, container, svg, Column, Space};
use iced::{Background, Border, Color, Element, Length, Theme};
use bds_core::i18n::UiLocale;
use crate::app::Message;
use crate::state::navigation::SidebarView;
/// Activity-bar short labels used in M2 (no SVG icons yet).
fn short_label(view: SidebarView) -> &'static str {
// ---------------------------------------------------------------------------
// SVG icon data — ported from bDS ActivityBar.tsx inline SVGs
// ---------------------------------------------------------------------------
fn icon_svg(view: SidebarView) -> &'static [u8] {
match view {
SidebarView::Posts => "Po",
SidebarView::Pages => "Pa",
SidebarView::Media => "Me",
SidebarView::Scripts => "Sc",
SidebarView::Templates => "Tp",
SidebarView::Tags => "Ta",
SidebarView::Chat => "AI",
SidebarView::Import => "Im",
SidebarView::Git => "Gi",
SidebarView::Settings => "Se",
SidebarView::Posts => include_bytes!("../../assets/icons/posts.svg"),
SidebarView::Pages => include_bytes!("../../assets/icons/pages.svg"),
SidebarView::Media => include_bytes!("../../assets/icons/media.svg"),
SidebarView::Scripts => include_bytes!("../../assets/icons/scripts.svg"),
SidebarView::Templates => include_bytes!("../../assets/icons/templates.svg"),
SidebarView::Tags => include_bytes!("../../assets/icons/tags.svg"),
SidebarView::Chat => include_bytes!("../../assets/icons/chat.svg"),
SidebarView::Import => include_bytes!("../../assets/icons/import.svg"),
SidebarView::Git => include_bytes!("../../assets/icons/git.svg"),
SidebarView::Settings => include_bytes!("../../assets/icons/settings.svg"),
}
}
@@ -40,28 +43,93 @@ const BOTTOM_ACTIVITIES: &[SidebarView] = &[
SidebarView::Settings,
];
// ---------------------------------------------------------------------------
// Styles — matching bDS ActivityBar.css
// ---------------------------------------------------------------------------
/// Active button: transparent bg.
fn active_button_style(_theme: &Theme, _status: button::Status) -> button::Style {
button::Style {
background: Some(Background::Color(Color::TRANSPARENT)),
text_color: Color::WHITE,
border: Border {
color: Color::TRANSPARENT,
width: 0.0,
radius: 0.0.into(),
},
..button::Style::default()
}
}
/// Inactive button: hover brightens background.
fn inactive_button_style(_theme: &Theme, status: button::Status) -> button::Style {
let bg = match status {
button::Status::Hovered => Color::from_rgb(0.22, 0.22, 0.27),
_ => Color::TRANSPARENT,
};
button::Style {
background: Some(Background::Color(bg)),
text_color: Color::from_rgb(0.55, 0.55, 0.60),
border: Border {
color: Color::TRANSPARENT,
width: 0.0,
radius: 0.0.into(),
},
..button::Style::default()
}
}
/// Activity bar container: dark background.
fn bar_background_style(_theme: &Theme) -> container::Style {
container::Style {
background: Some(Background::Color(Color::from_rgb(0.14, 0.14, 0.18))),
..container::Style::default()
}
}
// ---------------------------------------------------------------------------
// View
// ---------------------------------------------------------------------------
pub fn view(
active_view: SidebarView,
_locale: UiLocale,
) -> Element<'static, Message> {
let make_btn = |view: SidebarView| -> Element<'static, Message> {
let label = short_label(view);
let mut btn = button(
text(label).size(12).center(),
let handle = svg::Handle::from_memory(icon_svg(view));
let is_active = view == active_view;
// Render SVG: active = full opacity, inactive = 0.4 opacity (like bDS 60% opacity)
let icon = svg(handle)
.width(Length::Fixed(24.0))
.height(Length::Fixed(24.0))
.opacity(if is_active { 1.0 } else { 0.4 });
let btn = button(
container(icon)
.center_x(Length::Fixed(48.0))
.center_y(Length::Fixed(48.0)),
)
.width(Length::Fixed(48.0))
.height(Length::Fixed(48.0))
.on_press(Message::SetActiveView(view));
.padding(0)
.on_press(Message::SetActiveView(view))
.style(if is_active { active_button_style } else { inactive_button_style });
if view == active_view {
btn = btn.style(button::primary);
// Active indicator: 2px left border (like bDS/VS Code)
if is_active {
let indicator = container(Space::new(0, 0))
.width(Length::Fixed(2.0))
.height(Length::Fixed(48.0))
.style(|_theme: &Theme| container::Style {
background: Some(Background::Color(Color::WHITE)),
..container::Style::default()
});
iced::widget::row![indicator, btn].into()
} else {
btn = btn.style(button::secondary);
let spacer = Space::with_width(2.0);
iced::widget::row![spacer, btn].into()
}
container(btn)
.center_x(Length::Fixed(48.0))
.into()
};
let top_items: Vec<Element<'static, Message>> = TOP_ACTIVITIES
@@ -74,16 +142,21 @@ pub fn view(
.map(|v| make_btn(*v))
.collect();
let top = Column::with_children(top_items).spacing(2);
let bottom = Column::with_children(bottom_items).spacing(2);
let top = Column::with_children(top_items).spacing(0);
let bottom = Column::with_children(bottom_items).spacing(0);
column![
top,
Space::with_height(Length::Fill),
bottom,
]
.width(Length::Fixed(48.0))
container(
column![
top,
Space::with_height(Length::Fill),
bottom,
]
.width(Length::Fixed(50.0))
.height(Length::Fill)
.padding([4, 0]),
)
.width(Length::Fixed(50.0))
.height(Length::Fill)
.align_x(Alignment::Center)
.style(bar_background_style)
.into()
}

View File

@@ -1,5 +1,5 @@
use iced::widget::{button, column, container, row, scrollable, text, Space};
use iced::{Alignment, Element, Length};
use iced::{Alignment, Background, Border, Color, Element, Length, Theme};
use bds_core::i18n::UiLocale;
@@ -7,41 +7,83 @@ use crate::app::Message;
use crate::i18n::t;
use crate::state::navigation::{OutputEntry, PanelTab, TaskSnapshot};
/// Panel background style.
fn panel_style(_theme: &Theme) -> container::Style {
container::Style {
background: Some(Background::Color(Color::from_rgb(0.13, 0.13, 0.16))),
..container::Style::default()
}
}
/// Panel tab button — active.
fn tab_active(_theme: &Theme, _status: button::Status) -> button::Style {
button::Style {
background: Some(Background::Color(Color::from_rgb(0.20, 0.20, 0.25))),
text_color: Color::WHITE,
border: Border {
color: Color::from_rgb(0.30, 0.55, 0.90),
width: 0.0,
radius: 3.0.into(),
},
..button::Style::default()
}
}
/// Panel tab button — inactive.
fn tab_inactive(_theme: &Theme, status: button::Status) -> button::Style {
let bg = match status {
button::Status::Hovered => Color::from_rgb(0.18, 0.18, 0.22),
_ => Color::TRANSPARENT,
};
button::Style {
background: Some(Background::Color(bg)),
text_color: Color::from_rgb(0.60, 0.60, 0.65),
border: Border {
color: Color::TRANSPARENT,
width: 0.0,
radius: 3.0.into(),
},
..button::Style::default()
}
}
/// Close button style.
fn close_btn_style(_theme: &Theme, status: button::Status) -> button::Style {
let color = match status {
button::Status::Hovered => Color::WHITE,
_ => Color::from_rgb(0.50, 0.50, 0.55),
};
button::Style {
background: Some(Background::Color(Color::TRANSPARENT)),
text_color: color,
border: Border::default(),
..button::Style::default()
}
}
pub fn view(
panel_tab: PanelTab,
task_snapshots: &[TaskSnapshot],
output_entries: &[OutputEntry],
locale: UiLocale,
) -> Element<'static, Message> {
let muted = Color::from_rgb(0.50, 0.50, 0.55);
// Tab header
let tasks_btn = {
let mut btn = button(text(t(locale, "common.tasks")).size(12))
.on_press(Message::SetPanelTab(PanelTab::Tasks))
.padding([4, 8]);
if panel_tab == PanelTab::Tasks {
btn = btn.style(button::primary);
} else {
btn = btn.style(button::secondary);
}
btn
};
let tasks_btn = button(text(t(locale, "common.tasks")).size(12))
.on_press(Message::SetPanelTab(PanelTab::Tasks))
.padding([4, 8])
.style(if panel_tab == PanelTab::Tasks { tab_active } else { tab_inactive });
let output_btn = {
let mut btn = button(text(t(locale, "panel.output")).size(12))
.on_press(Message::SetPanelTab(PanelTab::Output))
.padding([4, 8]);
if panel_tab == PanelTab::Output {
btn = btn.style(button::primary);
} else {
btn = btn.style(button::secondary);
}
btn
};
let output_btn = button(text(t(locale, "panel.output")).size(12))
.on_press(Message::SetPanelTab(PanelTab::Output))
.padding([4, 8])
.style(if panel_tab == PanelTab::Output { tab_active } else { tab_inactive });
let close_btn = button(text("×").size(12))
let close_btn = button(text("\u{2715}").size(12))
.on_press(Message::TogglePanel)
.padding([4, 6])
.style(button::text);
.style(close_btn_style);
let tab_header = row![
tasks_btn,
@@ -50,13 +92,14 @@ pub fn view(
close_btn,
]
.spacing(4)
.align_y(Alignment::Center);
.align_y(Alignment::Center)
.padding([4, 8]);
// Tab content
let content: Element<'static, Message> = match panel_tab {
PanelTab::Tasks => {
if task_snapshots.is_empty() {
container(text(t(locale, "tasks.noActive")).size(12))
container(text(t(locale, "tasks.noActive")).size(12).color(muted))
.padding(8)
.into()
} else {
@@ -64,14 +107,14 @@ pub fn view(
.iter()
.map(|snap| {
let status_text = format!(
"{} {}{}",
"{} \u{2014} {}{}",
snap.label,
snap.status,
snap.progress
.map(|p| format!(" ({:.0}%)", p * 100.0))
.unwrap_or_default(),
);
text(status_text).size(11).into()
text(status_text).size(11).color(Color::from_rgb(0.70, 0.70, 0.75)).into()
})
.collect();
scrollable(
@@ -84,13 +127,18 @@ pub fn view(
}
PanelTab::Output => {
if output_entries.is_empty() {
container(text(t(locale, "panel.noOutput")).size(12))
container(text(t(locale, "panel.noOutput")).size(12).color(muted))
.padding(8)
.into()
} else {
let items: Vec<Element<'static, Message>> = output_entries
.iter()
.map(|entry| text(entry.text.clone()).size(11).into())
.map(|entry| {
text(entry.text.clone())
.size(11)
.color(Color::from_rgb(0.70, 0.70, 0.75))
.into()
})
.collect();
scrollable(
iced::widget::Column::with_children(items)
@@ -103,9 +151,10 @@ pub fn view(
};
container(
column![tab_header, content].spacing(4),
column![tab_header, content].spacing(0),
)
.width(Length::Fill)
.height(Length::Fixed(200.0))
.style(panel_style)
.into()
}

View File

@@ -1,5 +1,5 @@
use iced::widget::{column, container, scrollable, text};
use iced::{Element, Length};
use iced::widget::{column, container, scrollable, text, Space};
use iced::{Background, Border, Color, Element, Length, Theme};
use bds_core::i18n::UiLocale;
@@ -7,31 +7,57 @@ use crate::app::Message;
use crate::i18n::t;
use crate::state::navigation::SidebarView;
/// Sidebar container style — dark background with right border separator.
fn sidebar_style(_theme: &Theme) -> container::Style {
container::Style {
background: Some(Background::Color(Color::from_rgb(0.16, 0.16, 0.20))),
border: Border {
color: Color::from_rgb(0.25, 0.25, 0.30),
width: 1.0,
radius: 0.0.into(),
},
..container::Style::default()
}
}
/// Get the appropriate empty-state message key for each sidebar view.
fn placeholder_key(view: SidebarView) -> &'static str {
match view {
SidebarView::Posts => "sidebar.noPostsYet",
SidebarView::Pages => "sidebar.noPagesYet",
SidebarView::Media => "sidebar.noMediaYet",
SidebarView::Scripts => "sidebar.noScriptsYet",
SidebarView::Templates => "sidebar.noTemplatesYet",
SidebarView::Tags => "sidebar.tagsHeader",
SidebarView::Chat => "sidebar.chatPlaceholder",
SidebarView::Import => "sidebar.importPlaceholder",
SidebarView::Git => "sidebar.gitPlaceholder",
SidebarView::Settings => "sidebar.settingsHeader",
}
}
pub fn view(
sidebar_view: SidebarView,
locale: UiLocale,
) -> Element<'static, Message> {
let header_key = sidebar_view.i18n_key();
let header_text = t(locale, header_key);
let placeholder = match sidebar_view {
SidebarView::Posts => t(locale, "sidebar.noPostsYet"),
SidebarView::Pages => t(locale, "sidebar.noPagesYet"),
SidebarView::Media => t(locale, "sidebar.noMediaYet"),
SidebarView::Settings => t(locale, "sidebar.settingsHeader"),
SidebarView::Tags => t(locale, "sidebar.tagsHeader"),
_ => t(locale, "sidebar.loading"),
};
let header_text = t(locale, sidebar_view.i18n_key());
let placeholder = t(locale, placeholder_key(sidebar_view));
let content = column![
text(header_text).size(14),
text(placeholder).size(12),
text(header_text)
.size(13)
.color(Color::from_rgb(0.85, 0.85, 0.90)),
Space::with_height(8.0),
text(placeholder)
.size(12)
.color(Color::from_rgb(0.50, 0.50, 0.55)),
]
.spacing(8)
.spacing(4)
.padding(12);
container(scrollable(content))
.width(Length::Fixed(280.0))
.width(Length::Fixed(240.0))
.height(Length::Fill)
.style(sidebar_style)
.into()
}

View File

@@ -1,5 +1,5 @@
use iced::widget::{button, container, row, text, Space};
use iced::{Alignment, Element, Length};
use iced::widget::{button, container, pick_list, row, text, Space};
use iced::{Alignment, Background, Border, Color, Element, Length, Theme};
use bds_core::i18n::UiLocale;
@@ -7,6 +7,110 @@ use crate::app::Message;
use crate::i18n::{t, tw};
use crate::state::navigation::TaskSnapshot;
// ---------------------------------------------------------------------------
// Locale option wrapper for pick_list (needs ToString + PartialEq + Clone)
// ---------------------------------------------------------------------------
/// Wraps a UiLocale with a display label for the pick_list dropdown.
#[derive(Debug, Clone, PartialEq)]
struct LocaleOption {
locale: UiLocale,
label: String,
}
impl ToString for LocaleOption {
fn to_string(&self) -> String {
self.label.clone()
}
}
impl LocaleOption {
fn new(locale: UiLocale, display_locale: UiLocale) -> Self {
let key = match locale {
UiLocale::En => "language.en",
UiLocale::De => "language.de",
UiLocale::Fr => "language.fr",
UiLocale::It => "language.it",
UiLocale::Es => "language.es",
};
Self {
locale,
label: t(display_locale, key),
}
}
}
// ---------------------------------------------------------------------------
// Styles
// ---------------------------------------------------------------------------
/// Status bar background.
fn bar_style(_theme: &Theme) -> container::Style {
container::Style {
background: Some(Background::Color(Color::from_rgb(0.12, 0.12, 0.16))),
..container::Style::default()
}
}
/// Airplane button — inactive (dim).
fn airplane_inactive(_theme: &Theme, status: button::Status) -> button::Style {
let bg = match status {
button::Status::Hovered => Color::from_rgb(0.22, 0.22, 0.27),
_ => Color::TRANSPARENT,
};
button::Style {
background: Some(Background::Color(bg)),
text_color: Color::from_rgba(0.70, 0.70, 0.75, 0.4),
border: Border {
color: Color::TRANSPARENT,
width: 0.0,
radius: 3.0.into(),
},
..button::Style::default()
}
}
/// Airplane button — active (warning color).
fn airplane_active(_theme: &Theme, status: button::Status) -> button::Style {
let bg = match status {
button::Status::Hovered => Color::from_rgb(0.85, 0.65, 0.10),
_ => Color::from_rgb(0.75, 0.55, 0.05),
};
button::Style {
background: Some(Background::Color(bg)),
text_color: Color::WHITE,
border: Border {
color: Color::TRANSPARENT,
width: 0.0,
radius: 3.0.into(),
},
..button::Style::default()
}
}
/// Pick list style matching status bar.
fn locale_pick_list_style(_theme: &Theme, status: pick_list::Status) -> pick_list::Style {
let bg = match status {
pick_list::Status::Hovered => Color::from_rgb(0.22, 0.22, 0.27),
_ => Color::TRANSPARENT,
};
pick_list::Style {
text_color: Color::from_rgb(0.70, 0.70, 0.75),
placeholder_color: Color::from_rgb(0.50, 0.50, 0.55),
handle_color: Color::from_rgb(0.50, 0.50, 0.55),
background: Background::Color(bg),
border: Border {
color: Color::from_rgb(0.25, 0.25, 0.30),
width: 1.0,
radius: 3.0.into(),
},
}
}
// ---------------------------------------------------------------------------
// View
// ---------------------------------------------------------------------------
pub fn view(
active_project_name: Option<&str>,
post_count: usize,
@@ -15,9 +119,11 @@ pub fn view(
offline_mode: bool,
task_snapshots: &[TaskSnapshot],
) -> Element<'static, Message> {
// Left: project name + task indicator
let project_label = active_project_name.unwrap_or("").to_string();
let project_text = text(project_label).size(12);
let label_color = Color::from_rgb(0.60, 0.60, 0.65);
// ── Left: project name + task indicator ──
let project_label = active_project_name.unwrap_or("\u{2014}").to_string();
let project_text = text(project_label).size(12).color(Color::from_rgb(0.80, 0.80, 0.85));
let running: Vec<&TaskSnapshot> = task_snapshots
.iter()
@@ -26,9 +132,9 @@ pub fn view(
let task_indicator: Element<'static, Message> = if !running.is_empty() {
let first = running[0].label.clone();
if running.len() > 1 {
text(format!("{first} (+{})", running.len() - 1)).size(11).into()
text(format!("{first} (+{})", running.len() - 1)).size(11).color(label_color).into()
} else {
text(first).size(11).into()
text(first).size(11).color(label_color).into()
}
} else {
Space::with_width(0).into()
@@ -36,50 +142,44 @@ pub fn view(
let left = row![project_text, task_indicator].spacing(12);
// Right: post count, media count, airplane mode, locale selector, brand
// ── Right side ──
// Post + media counts
let posts_label = tw(locale, "statusBar.posts", &[("count", &post_count.to_string())]);
let media_label = tw(locale, "statusBar.media", &[("count", &media_count.to_string())]);
let airplane_label = if offline_mode {
t(locale, "statusBar.offlineModeActive")
} else {
t(locale, "statusBar.offlineMode")
};
let airplane_btn = button(text(airplane_label).size(11))
// Airplane mode toggle — just the ✈ icon, like bDS
let airplane_btn = button(text("\u{2708}").size(13))
.on_press(Message::SetOfflineMode(!offline_mode))
.padding([2, 6])
.style(if offline_mode { button::primary } else { button::text });
.padding([2, 4])
.style(if offline_mode { airplane_active } else { airplane_inactive });
let ui_label = t(locale, "statusBar.ui");
let locale_buttons: Vec<Element<'static, Message>> = UiLocale::all()
// Language selector — "UI" label + pick_list dropdown, like bDS
let options: Vec<LocaleOption> = UiLocale::all()
.iter()
.map(|l| {
let code = l.code().to_string();
let mut btn = button(text(code).size(11))
.on_press(Message::SetUiLocale(*l))
.padding([2, 4]);
if *l == locale {
btn = btn.style(button::primary);
} else {
btn = btn.style(button::text);
}
btn.into()
})
.map(|l| LocaleOption::new(*l, locale))
.collect();
let selected = Some(LocaleOption::new(locale, locale));
let locale_row = iced::widget::Row::with_children(locale_buttons).spacing(2);
let locale_picker = pick_list(
options,
selected,
|opt: LocaleOption| Message::SetUiLocale(opt.locale),
)
.text_size(11)
.padding([2, 4])
.width(Length::Shrink)
.style(locale_pick_list_style);
let right = row![
text(posts_label).size(11),
text(media_label).size(11),
text(posts_label).size(11).color(label_color),
text(media_label).size(11).color(label_color),
airplane_btn,
text(ui_label).size(11),
locale_row,
text("bDS").size(11),
text(t(locale, "statusBar.ui")).size(11).color(label_color),
locale_picker,
text("bDS").size(11).color(Color::from_rgb(0.45, 0.45, 0.50)),
]
.spacing(12)
.spacing(8)
.align_y(Alignment::Center);
container(
@@ -92,6 +192,7 @@ pub fn view(
.padding([0, 8]),
)
.width(Length::Fill)
.height(Length::Fixed(22.0))
.height(Length::Fixed(24.0))
.style(bar_style)
.into()
}

View File

@@ -1,15 +1,78 @@
use iced::widget::{button, container, row, text, Space};
use iced::{Element, Length};
use iced::{Background, Border, Color, Element, Length, Theme};
use crate::app::Message;
use crate::state::tabs::Tab;
/// Tab bar background.
fn bar_style(_theme: &Theme) -> container::Style {
container::Style {
background: Some(Background::Color(Color::from_rgb(0.14, 0.14, 0.18))),
border: Border {
color: Color::from_rgb(0.25, 0.25, 0.30),
width: 0.0,
radius: 0.0.into(),
},
..container::Style::default()
}
}
/// Active tab style.
fn tab_active(_theme: &Theme, _status: button::Status) -> button::Style {
button::Style {
background: Some(Background::Color(Color::from_rgb(0.11, 0.11, 0.14))),
text_color: Color::WHITE,
border: Border {
color: Color::from_rgb(0.30, 0.55, 0.90),
width: 0.0,
radius: 4.0.into(),
},
..button::Style::default()
}
}
/// Inactive tab style.
fn tab_inactive(_theme: &Theme, status: button::Status) -> button::Style {
let bg = match status {
button::Status::Hovered => Color::from_rgb(0.18, 0.18, 0.22),
_ => Color::from_rgb(0.14, 0.14, 0.18),
};
button::Style {
background: Some(Background::Color(bg)),
text_color: Color::from_rgb(0.60, 0.60, 0.65),
border: Border {
color: Color::TRANSPARENT,
width: 0.0,
radius: 4.0.into(),
},
..button::Style::default()
}
}
/// Close button on tab.
fn close_style(_theme: &Theme, status: button::Status) -> button::Style {
let color = match status {
button::Status::Hovered => Color::WHITE,
_ => Color::from_rgb(0.45, 0.45, 0.50),
};
button::Style {
background: Some(Background::Color(Color::TRANSPARENT)),
text_color: color,
border: Border::default(),
..button::Style::default()
}
}
pub fn view(
tabs: &[Tab],
active_tab: Option<&str>,
) -> Element<'static, Message> {
if tabs.is_empty() {
return Space::with_height(0).into();
return container(Space::new(0, 0))
.width(Length::Fill)
.height(Length::Fixed(35.0))
.style(bar_style)
.into();
}
let tab_buttons: Vec<Element<'static, Message>> = tabs
@@ -17,7 +80,7 @@ pub fn view(
.map(|tab| {
let is_active = active_tab == Some(tab.id.as_str());
let title_text = if tab.is_transient {
format!("{} (preview)", tab.title)
format!("{} \u{25CB}", tab.title) // hollow circle for transient
} else {
tab.title.clone()
};
@@ -26,24 +89,19 @@ pub fn view(
let label = row![
text(title_text).size(12),
button(text("×").size(12))
button(text("\u{2715}").size(10))
.on_press(Message::CloseTab(close_id))
.padding(2)
.style(button::text),
.style(close_style),
]
.spacing(4);
.spacing(6)
.align_y(iced::Alignment::Center);
let mut btn = button(label)
button(label)
.on_press(Message::SelectTab(tab_id))
.padding([4, 8]);
if is_active {
btn = btn.style(button::primary);
} else {
btn = btn.style(button::secondary);
}
btn.into()
.padding([6, 12])
.style(if is_active { tab_active } else { tab_inactive })
.into()
})
.collect();
@@ -54,5 +112,6 @@ pub fn view(
)
.width(Length::Fill)
.height(Length::Fixed(35.0))
.style(bar_style)
.into()
}

View File

@@ -1,5 +1,5 @@
use iced::widget::{column, container, text};
use iced::{Element, Length};
use iced::{Background, Color, Element, Length, Theme};
use bds_core::i18n::UiLocale;
@@ -12,8 +12,12 @@ pub fn view(locale: UiLocale) -> Element<'static, Message> {
container(
column![
text(title).size(28),
text(subtitle).size(14),
text(title)
.size(28)
.color(Color::from_rgb(0.85, 0.85, 0.90)),
text(subtitle)
.size(14)
.color(Color::from_rgb(0.55, 0.55, 0.60)),
]
.spacing(12)
.align_x(iced::Alignment::Center),
@@ -22,5 +26,9 @@ pub fn view(locale: UiLocale) -> Element<'static, Message> {
.height(Length::Fill)
.center_x(Length::Fill)
.center_y(Length::Fill)
.style(|_theme: &Theme| container::Style {
background: Some(Background::Color(Color::from_rgb(0.11, 0.11, 0.14))),
..container::Style::default()
})
.into()
}

View File

@@ -1,5 +1,5 @@
use iced::widget::{column, row};
use iced::{Element, Length};
use iced::widget::{column, container, row, Space};
use iced::{Background, Color, Element, Length, Theme};
use bds_core::i18n::UiLocale;
@@ -8,6 +8,36 @@ use crate::state::navigation::{OutputEntry, PanelTab, SidebarView, TaskSnapshot}
use crate::state::tabs::Tab;
use crate::views::{activity_bar, panel, sidebar, status_bar, tab_bar, welcome};
/// Main content area background.
fn content_bg(_theme: &Theme) -> container::Style {
container::Style {
background: Some(Background::Color(Color::from_rgb(0.11, 0.11, 0.14))),
..container::Style::default()
}
}
/// Horizontal separator line between regions.
fn separator_v() -> iced::widget::Container<'static, Message> {
container(Space::new(0, 0))
.width(Length::Fixed(1.0))
.height(Length::Fill)
.style(|_theme: &Theme| container::Style {
background: Some(Background::Color(Color::from_rgb(0.25, 0.25, 0.30))),
..container::Style::default()
})
}
/// Horizontal line separator (full width).
fn separator_h() -> iced::widget::Container<'static, Message> {
container(Space::new(0, 0))
.width(Length::Fill)
.height(Length::Fixed(1.0))
.style(|_theme: &Theme| container::Style {
background: Some(Background::Color(Color::from_rgb(0.25, 0.25, 0.30))),
..container::Style::default()
})
}
/// Compose the full workspace layout.
pub fn view(
// Navigation
@@ -32,38 +62,31 @@ pub fn view(
// Activity bar (leftmost column)
let activity = activity_bar::view(sidebar_view, locale);
// Sidebar (conditionally visible)
let sidebar_el: Option<Element<'static, Message>> = if sidebar_visible {
Some(sidebar::view(sidebar_view, locale))
} else {
None
};
// Tab bar
let tabs_el = tab_bar::view(tabs, active_tab);
// Content area
let content_area = welcome::view(locale);
// Panel (conditionally visible)
let panel_el: Option<Element<'static, Message>> = if panel_visible {
Some(panel::view(panel_tab, task_snapshots, output_entries, locale))
} else {
None
};
// Right column: tab bar + content + panel
let mut right_col = column![tabs_el, content_area];
if let Some(p) = panel_el {
right_col = right_col.push(p);
if panel_visible {
right_col = right_col.push(separator_h());
right_col = right_col.push(panel::view(panel_tab, task_snapshots, output_entries, locale));
}
let right = right_col.width(Length::Fill).height(Length::Fill);
let right = container(right_col.width(Length::Fill).height(Length::Fill))
.width(Length::Fill)
.height(Length::Fill)
.style(content_bg);
// Main row: activity bar + sidebar? + right column
// Main row: activity bar | separator | sidebar? | separator | right column
let mut main_row = row![activity];
if let Some(sb) = sidebar_el {
main_row = main_row.push(sb);
if sidebar_visible {
main_row = main_row.push(sidebar::view(sidebar_view, locale));
main_row = main_row.push(separator_v());
}
main_row = main_row.push(right);
let main_row = main_row.height(Length::Fill);
@@ -77,7 +100,7 @@ pub fn view(
task_snapshots,
);
column![main_row, status]
column![main_row, separator_h(), status]
.width(Length::Fill)
.height(Length::Fill)
.into()