feat(ui): refine drawing tabs

Use theme-driven active and hover states, integrate close controls, and keep tab spacing stable across drawing states.

Refs #481
This commit is contained in:
Hakan Seven committed 2026-08-04 19:04:59 +03:00
1 parent 9cf0c9f945
commit 7c893d8f5f
3 files changed
+109 -28

No files matched your search

+4
View File
@@ -263,6 +263,7 @@ pub(super) struct OpenCADStudio {
start: Instant,
tabs: Vec<DocumentTab>,
active_tab: usize,
hovered_doc_tab: Option<usize>,
tab_counter: usize,
ribbon: Ribbon,
/// Recently opened files, newest first — backs the Start page panel.
@@ -1730,6 +1731,8 @@ pub enum Message {
TabNew,
/// Switch to the given tab index.
TabSwitch(usize),
/// Drawing tab currently under the pointer; controls integrated close affordance.
DocTabHover(Option<usize>),
/// Move a drawing tab before/after another drawing tab.
TabReorder {
from: usize,
@@ -2834,6 +2837,7 @@ impl OpenCADStudio {
start: Instant::now(),
tabs: vec![start_tab],
active_tab: 0,
hovered_doc_tab: None,
tab_counter: 0,
ribbon: Ribbon::new(),
// Populated from the consolidated config after construction
+10 -1
View File
@@ -1333,6 +1333,11 @@ impl OpenCADStudio {
Task::none()
}
Message::DocTabHover(index) => {
self.hovered_doc_tab = index.filter(|&idx| idx < self.tabs.len());
Task::none()
}
Message::TabReorder { from, to, after } => {
let Some(insertion) =
reorder_insertion_index(from, to, after, self.tabs.len())
@@ -1351,10 +1356,14 @@ impl OpenCADStudio {
if let Some(index) = self.tabs.iter().position(|tab| tab.id == active_id) {
self.active_tab = index;
}
self.hovered_doc_tab = None;
Task::none()
}
Message::TabClose(idx) => self.on_tab_close(idx),
Message::TabClose(idx) => {
self.hovered_doc_tab = None;
self.on_tab_close(idx)
}
Message::DocTabSaveAll => self.dispatch_command("SAVEALL"),
+95 -27
View File
@@ -1575,7 +1575,11 @@ impl OpenCADStudio {
));
}
if self.show_file_tabs {
col = col.push(doc_tab_bar(&self.tabs, self.active_tab));
col = col.push(doc_tab_bar(
&self.tabs,
self.active_tab,
self.hovered_doc_tab,
));
}
col.push(center_stack)
.push({
@@ -2032,7 +2036,11 @@ fn doc_tab_context_menu(
.into()
}
pub(super) fn doc_tab_bar<'a>(tabs: &'a [DocumentTab], active_tab: usize) -> Element<'a, Message> {
pub(super) fn doc_tab_bar<'a>(
tabs: &'a [DocumentTab],
active_tab: usize,
hovered_tab: Option<usize>,
) -> Element<'a, Message> {
// Document tabs live in a flex-wrap flow so they spill onto lower rows when
// there are more tabs than the width can hold on one line.
let mut items: Vec<Element<'_, Message>> = Vec::new();
@@ -2045,6 +2053,7 @@ pub(super) fn doc_tab_bar<'a>(tabs: &'a [DocumentTab], active_tab: usize) -> Ele
for (idx, tab) in tabs.iter().enumerate() {
let is_active = idx == active_tab;
let is_hovered = hovered_tab == Some(idx);
let name = crate::ui::text_util::elide(&tab.tab_display_name(), 24);
let title_inner: Element<'_, Message> = if tab.dirty {
row![
@@ -2061,19 +2070,15 @@ pub(super) fn doc_tab_bar<'a>(tabs: &'a [DocumentTab], active_tab: usize) -> Ele
let title_btn = button(title_inner)
.on_press(Message::TabSwitch(idx))
.height(Fill)
.padding([4, 12])
.style(move |theme: &Theme, status| {
.padding([5, 14])
.style(move |theme: &Theme, _status| {
let palette = theme.palette();
let background = match (is_active, status) {
(false, button::Status::Hovered) => {
Some(Background::Color(palette.background.weak.color))
}
_ => None,
};
button::Style {
background,
background: None,
text_color: if is_active {
palette.primary.weak.text
} else if is_hovered {
palette.background.weak.text
} else {
palette.background.base.text.scale_alpha(0.72)
},
@@ -2100,8 +2105,32 @@ pub(super) fn doc_tab_bar<'a>(tabs: &'a [DocumentTab], active_tab: usize) -> Ele
let close_btn = button(text("×").size(12))
.on_press(Message::TabClose(idx))
.height(Fill)
.padding([4, 8])
.style(button::subtle);
.padding([5, 9])
.style(move |theme: &Theme, status| {
let palette = theme.palette();
button::Style {
background: matches!(
status,
button::Status::Hovered | button::Status::Pressed
)
.then_some(Background::Color(palette.warning.weak.color)),
text_color: if matches!(
status,
button::Status::Hovered | button::Status::Pressed
) {
palette.warning.weak.text
} else if is_active {
palette.primary.weak.text
} else if is_hovered {
palette.background.weak.text
} else {
palette.background.base.text.scale_alpha(0.72)
},
border: Border::default(),
shadow: iced::Shadow::default(),
snap: false,
}
});
row![title_btn, close_btn]
.spacing(0)
.height(Fill)
@@ -2109,22 +2138,25 @@ pub(super) fn doc_tab_bar<'a>(tabs: &'a [DocumentTab], active_tab: usize) -> Ele
};
let tab_container = container(row_inner)
.height(iced::Length::Fixed(23.0))
.height(iced::Length::Fixed(28.0))
.style(move |theme: &Theme| {
let palette = theme.palette();
let background = if is_active {
palette.primary.weak.color
} else if is_hovered {
palette.background.weak.color
} else {
palette.background.base.color
};
container::Style {
background: Some(Background::Color(if is_active {
palette.primary.weak.color
} else {
palette.background.base.color
})),
background: Some(Background::Color(background)),
border: Border {
color: if is_active {
palette.primary.base.color
} else {
Color::TRANSPARENT
palette.background.neutral.color
},
width: if is_active { 1.0 } else { 0.0 },
width: 1.0,
radius: 0.0.into(),
},
..Default::default()
@@ -2157,22 +2189,58 @@ pub(super) fn doc_tab_bar<'a>(tabs: &'a [DocumentTab], active_tab: usize) -> Ele
)
.into()
};
items.push(tab_element);
items.push(
mouse_area(tab_element)
.on_enter(Message::DocTabHover(Some(idx)))
.on_exit(Message::DocTabHover(None))
.into(),
);
}
let new_btn = button(text("+").size(14))
.on_press(Message::TabNew)
.padding([4, 10])
.style(button::subtle);
.height(iced::Length::Fixed(28.0))
.padding([5, 10])
.style(|theme: &Theme, status| {
let palette = theme.palette();
let hovered = matches!(
status,
button::Status::Hovered | button::Status::Pressed
);
button::Style {
background: Some(Background::Color(if hovered {
palette.background.weak.color
} else {
palette.background.base.color
})),
text_color: palette.background.base.text,
border: Border {
color: palette.background.neutral.color,
width: 1.0,
radius: 3.0.into(),
},
shadow: iced::Shadow::default(),
snap: false,
}
});
items.push(new_btn.into());
items.push(
container(new_btn)
.padding(iced::Padding {
top: 0.0,
right: 0.0,
bottom: 0.0,
left: 6.0,
})
.into(),
);
container(
Row::with_children(items)
.spacing(0.0)
.align_y(iced::Center)
.wrap()
.vertical_spacing(0.0),
.vertical_spacing(2.0),
)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
@@ -2186,7 +2254,7 @@ pub(super) fn doc_tab_bar<'a>(tabs: &'a [DocumentTab], active_tab: usize) -> Ele
..Default::default()
})
.width(Fill)
.padding([0, 2])
.padding([2, 2])
.into()
}