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:
parent
9cf0c9f945
commit
7c893d8f5f
3 changed files with 109 additions and 28 deletions
|
|
@ -263,6 +263,7 @@ pub(super) struct OpenCADStudio {
|
||||||
start: Instant,
|
start: Instant,
|
||||||
tabs: Vec<DocumentTab>,
|
tabs: Vec<DocumentTab>,
|
||||||
active_tab: usize,
|
active_tab: usize,
|
||||||
|
hovered_doc_tab: Option<usize>,
|
||||||
tab_counter: usize,
|
tab_counter: usize,
|
||||||
ribbon: Ribbon,
|
ribbon: Ribbon,
|
||||||
/// Recently opened files, newest first — backs the Start page panel.
|
/// Recently opened files, newest first — backs the Start page panel.
|
||||||
|
|
@ -1730,6 +1731,8 @@ pub enum Message {
|
||||||
TabNew,
|
TabNew,
|
||||||
/// Switch to the given tab index.
|
/// Switch to the given tab index.
|
||||||
TabSwitch(usize),
|
TabSwitch(usize),
|
||||||
|
/// Drawing tab currently under the pointer; controls integrated close affordance.
|
||||||
|
DocTabHover(Option<usize>),
|
||||||
/// Move a drawing tab before/after another drawing tab.
|
/// Move a drawing tab before/after another drawing tab.
|
||||||
TabReorder {
|
TabReorder {
|
||||||
from: usize,
|
from: usize,
|
||||||
|
|
@ -2834,6 +2837,7 @@ impl OpenCADStudio {
|
||||||
start: Instant::now(),
|
start: Instant::now(),
|
||||||
tabs: vec![start_tab],
|
tabs: vec![start_tab],
|
||||||
active_tab: 0,
|
active_tab: 0,
|
||||||
|
hovered_doc_tab: None,
|
||||||
tab_counter: 0,
|
tab_counter: 0,
|
||||||
ribbon: Ribbon::new(),
|
ribbon: Ribbon::new(),
|
||||||
// Populated from the consolidated config after construction
|
// Populated from the consolidated config after construction
|
||||||
|
|
|
||||||
|
|
@ -1333,6 +1333,11 @@ impl OpenCADStudio {
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Message::DocTabHover(index) => {
|
||||||
|
self.hovered_doc_tab = index.filter(|&idx| idx < self.tabs.len());
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
Message::TabReorder { from, to, after } => {
|
Message::TabReorder { from, to, after } => {
|
||||||
let Some(insertion) =
|
let Some(insertion) =
|
||||||
reorder_insertion_index(from, to, after, self.tabs.len())
|
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) {
|
if let Some(index) = self.tabs.iter().position(|tab| tab.id == active_id) {
|
||||||
self.active_tab = index;
|
self.active_tab = index;
|
||||||
}
|
}
|
||||||
|
self.hovered_doc_tab = None;
|
||||||
Task::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"),
|
Message::DocTabSaveAll => self.dispatch_command("SAVEALL"),
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1575,7 +1575,11 @@ impl OpenCADStudio {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if self.show_file_tabs {
|
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)
|
col.push(center_stack)
|
||||||
.push({
|
.push({
|
||||||
|
|
@ -2032,7 +2036,11 @@ fn doc_tab_context_menu(
|
||||||
.into()
|
.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
|
// 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.
|
// there are more tabs than the width can hold on one line.
|
||||||
let mut items: Vec<Element<'_, Message>> = Vec::new();
|
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() {
|
for (idx, tab) in tabs.iter().enumerate() {
|
||||||
let is_active = idx == active_tab;
|
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 name = crate::ui::text_util::elide(&tab.tab_display_name(), 24);
|
||||||
let title_inner: Element<'_, Message> = if tab.dirty {
|
let title_inner: Element<'_, Message> = if tab.dirty {
|
||||||
row![
|
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)
|
let title_btn = button(title_inner)
|
||||||
.on_press(Message::TabSwitch(idx))
|
.on_press(Message::TabSwitch(idx))
|
||||||
.height(Fill)
|
.height(Fill)
|
||||||
.padding([4, 12])
|
.padding([5, 14])
|
||||||
.style(move |theme: &Theme, status| {
|
.style(move |theme: &Theme, _status| {
|
||||||
let palette = theme.palette();
|
let palette = theme.palette();
|
||||||
let background = match (is_active, status) {
|
|
||||||
(false, button::Status::Hovered) => {
|
|
||||||
Some(Background::Color(palette.background.weak.color))
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
button::Style {
|
button::Style {
|
||||||
background,
|
background: None,
|
||||||
text_color: if is_active {
|
text_color: if is_active {
|
||||||
palette.primary.weak.text
|
palette.primary.weak.text
|
||||||
|
} else if is_hovered {
|
||||||
|
palette.background.weak.text
|
||||||
} else {
|
} else {
|
||||||
palette.background.base.text.scale_alpha(0.72)
|
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))
|
let close_btn = button(text("×").size(12))
|
||||||
.on_press(Message::TabClose(idx))
|
.on_press(Message::TabClose(idx))
|
||||||
.height(Fill)
|
.height(Fill)
|
||||||
.padding([4, 8])
|
.padding([5, 9])
|
||||||
.style(button::subtle);
|
.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]
|
row![title_btn, close_btn]
|
||||||
.spacing(0)
|
.spacing(0)
|
||||||
.height(Fill)
|
.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)
|
let tab_container = container(row_inner)
|
||||||
.height(iced::Length::Fixed(23.0))
|
.height(iced::Length::Fixed(28.0))
|
||||||
.style(move |theme: &Theme| {
|
.style(move |theme: &Theme| {
|
||||||
let palette = theme.palette();
|
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 {
|
container::Style {
|
||||||
background: Some(Background::Color(if is_active {
|
background: Some(Background::Color(background)),
|
||||||
palette.primary.weak.color
|
|
||||||
} else {
|
|
||||||
palette.background.base.color
|
|
||||||
})),
|
|
||||||
border: Border {
|
border: Border {
|
||||||
color: if is_active {
|
color: if is_active {
|
||||||
palette.primary.base.color
|
palette.primary.base.color
|
||||||
} else {
|
} else {
|
||||||
Color::TRANSPARENT
|
palette.background.neutral.color
|
||||||
},
|
},
|
||||||
width: if is_active { 1.0 } else { 0.0 },
|
width: 1.0,
|
||||||
radius: 0.0.into(),
|
radius: 0.0.into(),
|
||||||
},
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
|
@ -2157,22 +2189,58 @@ pub(super) fn doc_tab_bar<'a>(tabs: &'a [DocumentTab], active_tab: usize) -> Ele
|
||||||
)
|
)
|
||||||
.into()
|
.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))
|
let new_btn = button(text("+").size(14))
|
||||||
.on_press(Message::TabNew)
|
.on_press(Message::TabNew)
|
||||||
.padding([4, 10])
|
.height(iced::Length::Fixed(28.0))
|
||||||
.style(button::subtle);
|
.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(
|
container(
|
||||||
Row::with_children(items)
|
Row::with_children(items)
|
||||||
.spacing(0.0)
|
.spacing(0.0)
|
||||||
.align_y(iced::Center)
|
.align_y(iced::Center)
|
||||||
.wrap()
|
.wrap()
|
||||||
.vertical_spacing(0.0),
|
.vertical_spacing(2.0),
|
||||||
)
|
)
|
||||||
.style(|theme: &Theme| container::Style {
|
.style(|theme: &Theme| container::Style {
|
||||||
background: Some(Background::Color(
|
background: Some(Background::Color(
|
||||||
|
|
@ -2186,7 +2254,7 @@ pub(super) fn doc_tab_bar<'a>(tabs: &'a [DocumentTab], active_tab: usize) -> Ele
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
.width(Fill)
|
.width(Fill)
|
||||||
.padding([0, 2])
|
.padding([2, 2])
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue