fix: menues now working, also project selector

This commit is contained in:
2026-04-04 20:39:29 +02:00
parent e2ef6c925a
commit 1e6de6ac1c
8 changed files with 318 additions and 27 deletions

View File

@@ -4,4 +4,5 @@ pub mod sidebar;
pub mod tab_bar;
pub mod status_bar;
pub mod panel;
pub mod project_selector;
pub mod welcome;

View File

@@ -0,0 +1,226 @@
use iced::widget::{button, container, row, svg, text, Column, Space};
use iced::{Background, Border, Color, Element, Length, Theme};
use bds_core::i18n::UiLocale;
use bds_core::model::Project;
use crate::app::Message;
use crate::i18n::t;
// ---------------------------------------------------------------------------
// Styles
// ---------------------------------------------------------------------------
fn dropdown_bg(_theme: &Theme) -> container::Style {
container::Style {
background: Some(Background::Color(Color::from_rgb(0.18, 0.18, 0.22))),
border: Border {
color: Color::from_rgb(0.30, 0.30, 0.35),
width: 1.0,
radius: 4.0.into(),
},
..container::Style::default()
}
}
fn project_item(_theme: &Theme, status: button::Status) -> button::Style {
let bg = match status {
button::Status::Hovered => Color::from_rgb(0.25, 0.25, 0.30),
_ => Color::from_rgb(0.18, 0.18, 0.22),
};
button::Style {
background: Some(Background::Color(bg)),
text_color: Color::WHITE,
border: Border {
color: Color::TRANSPARENT,
width: 0.0,
radius: 2.0.into(),
},
..button::Style::default()
}
}
fn project_item_active(_theme: &Theme, status: button::Status) -> button::Style {
let bg = match status {
button::Status::Hovered => Color::from_rgb(0.25, 0.32, 0.45),
_ => Color::from_rgb(0.20, 0.28, 0.40),
};
button::Style {
background: Some(Background::Color(bg)),
text_color: Color::WHITE,
border: Border {
color: Color::TRANSPARENT,
width: 0.0,
radius: 2.0.into(),
},
..button::Style::default()
}
}
fn new_project_btn(_theme: &Theme, status: button::Status) -> button::Style {
let bg = match status {
button::Status::Hovered => Color::from_rgb(0.25, 0.25, 0.30),
_ => Color::TRANSPARENT,
};
button::Style {
background: Some(Background::Color(bg)),
text_color: Color::from_rgb(0.55, 0.75, 0.95),
border: Border {
color: Color::TRANSPARENT,
width: 0.0,
radius: 2.0.into(),
},
..button::Style::default()
}
}
fn header_style(_theme: &Theme) -> container::Style {
container::Style {
border: Border {
color: Color::from_rgb(0.30, 0.30, 0.35),
width: 0.0,
radius: 0.0.into(),
},
..container::Style::default()
}
}
// ---------------------------------------------------------------------------
// Folder icon SVG (16x16)
// ---------------------------------------------------------------------------
const FOLDER_ICON: &[u8] = include_bytes!("../../assets/icons/folder.svg");
// ---------------------------------------------------------------------------
// View: renders the dropdown content (project list + new project)
// ---------------------------------------------------------------------------
pub fn view(
projects: &[Project],
active_project_id: Option<&str>,
locale: UiLocale,
) -> Element<'static, Message> {
let header = container(
text(t(locale, "projectSelector.projectsHeader"))
.size(11)
.color(Color::from_rgb(0.55, 0.55, 0.60)),
)
.padding([4, 8])
.style(header_style);
let mut items: Vec<Element<'static, Message>> = Vec::new();
items.push(header.into());
for project in projects {
let is_active = active_project_id == Some(project.id.as_str());
let name = project.name.clone();
let id = project.id.clone();
let label = if is_active {
row![
text("\u{2713}").size(12).color(Color::from_rgb(0.40, 0.80, 0.40)),
text(name).size(12).color(Color::WHITE),
]
.spacing(6)
} else {
row![
Space::with_width(Length::Fixed(14.0)),
text(name).size(12).color(Color::from_rgb(0.80, 0.80, 0.85)),
]
.spacing(6)
};
let style_fn = if is_active { project_item_active } else { project_item };
items.push(
button(label)
.on_press(Message::SwitchProject(id))
.padding([4, 8])
.width(Length::Fill)
.style(style_fn)
.into(),
);
}
// Separator
items.push(
container(Space::new(Length::Fill, Length::Fixed(1.0)))
.style(|_theme: &Theme| container::Style {
background: Some(Background::Color(Color::from_rgb(0.30, 0.30, 0.35))),
..container::Style::default()
})
.width(Length::Fill)
.into(),
);
// New project button
items.push(
button(
row![
text("+").size(14).color(Color::from_rgb(0.55, 0.75, 0.95)),
text(t(locale, "projectSelector.newProject")).size(12),
]
.spacing(6),
)
.on_press(Message::RequestCreateProject)
.padding([4, 8])
.width(Length::Fill)
.style(new_project_btn)
.into(),
);
container(
Column::with_children(items)
.spacing(2)
.padding(4)
.width(Length::Fixed(200.0)),
)
.style(dropdown_bg)
.into()
}
// ---------------------------------------------------------------------------
// Trigger button for status bar (folder icon + project name + chevron)
// ---------------------------------------------------------------------------
pub fn trigger_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::WHITE,
border: Border {
color: Color::TRANSPARENT,
width: 0.0,
radius: 3.0.into(),
},
..button::Style::default()
}
}
pub fn trigger_button(project_name: &str) -> Element<'static, Message> {
let folder = svg::Handle::from_memory(FOLDER_ICON);
let folder_icon = svg(folder)
.width(Length::Fixed(14.0))
.height(Length::Fixed(14.0));
let name = text(project_name.to_string())
.size(12)
.color(Color::from_rgb(0.80, 0.80, 0.85));
let chevron = text("\u{25BE}")
.size(10)
.color(Color::from_rgb(0.55, 0.55, 0.60));
button(
row![folder_icon, name, chevron]
.spacing(4)
.align_y(iced::Alignment::Center),
)
.on_press(Message::ToggleProjectDropdown)
.padding([2, 6])
.style(trigger_style)
.into()
}

View File

@@ -7,6 +7,7 @@ use bds_core::i18n::UiLocale;
use crate::app::Message;
use crate::i18n::tw;
use crate::state::navigation::TaskSnapshot;
use crate::views::project_selector;
// ---------------------------------------------------------------------------
// Styles
@@ -119,9 +120,9 @@ pub fn view(
) -> Element<'static, Message> {
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));
// ── Left: project selector trigger + task indicator ──
let project_name = active_project_name.unwrap_or("\u{2014}");
let project_trigger = project_selector::trigger_button(project_name);
let running: Vec<&TaskSnapshot> = task_snapshots
.iter()
@@ -138,7 +139,9 @@ pub fn view(
Space::with_width(0).into()
};
let left = row![project_text, task_indicator].spacing(12);
let left = row![project_trigger, task_indicator]
.spacing(12)
.align_y(Alignment::Center);
// ── Right side ──

View File

@@ -3,11 +3,12 @@ use iced::widget::text::Shaping;
use iced::{Alignment, Background, Color, Element, Length, Padding, Theme};
use bds_core::i18n::UiLocale;
use bds_core::model::Project;
use crate::app::Message;
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};
use crate::views::{activity_bar, panel, project_selector, sidebar, status_bar, tab_bar, welcome};
/// Main content area background.
fn content_bg(_theme: &Theme) -> container::Style {
@@ -54,10 +55,13 @@ pub fn view(
output_entries: &[OutputEntry],
// Status bar
active_project_name: Option<&str>,
projects: &[Project],
active_project_id: Option<&str>,
post_count: usize,
media_count: usize,
offline_mode: bool,
locale_dropdown_open: bool,
project_dropdown_open: bool,
// i18n
locale: UiLocale,
) -> Element<'static, Message> {
@@ -107,8 +111,8 @@ pub fn view(
.height(Length::Fill)
.into();
if locale_dropdown_open {
// Build the dropdown menu overlay
// Overlay: either locale dropdown or project dropdown (mutually exclusive)
let overlay: Option<Element<'static, Message>> = if locale_dropdown_open {
let items: Vec<Element<'static, Message>> = UiLocale::all()
.iter()
.map(|&l| {
@@ -129,25 +133,52 @@ pub fn view(
)
.style(status_bar::dropdown_bg);
// Position the dropdown at bottom-right, above the status bar (24px)
let overlay: Element<'static, Message> = container(
// Position at bottom-right, above status bar
Some(
container(
row![
Space::with_width(Length::Fill),
dropdown_menu,
// Offset from right edge to align near the flag trigger
Space::with_width(Length::Fixed(40.0)),
]
container(
row![
Space::with_width(Length::Fill),
dropdown_menu,
Space::with_width(Length::Fixed(40.0)),
]
)
.width(Length::Fill)
.align_y(Alignment::End)
.padding(Padding { top: 0.0, right: 0.0, bottom: 25.0, left: 0.0 })
)
.width(Length::Fill)
.height(Length::Fill)
.align_y(Alignment::End)
.padding(Padding { top: 0.0, right: 0.0, bottom: 25.0, left: 0.0 }) // above status bar
.into()
)
.width(Length::Fill)
.height(Length::Fill)
.align_y(Alignment::End)
.into();
} else if project_dropdown_open {
let dropdown = project_selector::view(projects, active_project_id, locale);
// Position at bottom-left, above status bar
Some(
container(
container(
row![
Space::with_width(Length::Fixed(8.0)),
dropdown,
Space::with_width(Length::Fill),
]
)
.width(Length::Fill)
.align_y(Alignment::End)
.padding(Padding { top: 0.0, right: 0.0, bottom: 25.0, left: 0.0 })
)
.width(Length::Fill)
.height(Length::Fill)
.align_y(Alignment::End)
.into()
)
} else {
None
};
if let Some(overlay) = overlay {
stack![base_layout, overlay]
.width(Length::Fill)
.height(Length::Fill)