b09795ccff
- Add `persistence.rs` for stable serialization of dock layouts without runtime identifiers. - Introduce `preview.rs` for geometry handling of dock drop previews. - Create `sizing.rs` to manage content-aware sizing policies and constraint solving for dock panels. - Implement `welcome.rs` to provide a welcome surface when no documents are open, featuring New and Open actions.
674 lines
26 KiB
Rust
674 lines
26 KiB
Rust
//! Dock view — renders the PaneGrid with panel content.
|
||
//!
|
||
//! Each pane type maps to its corresponding panel view function.
|
||
//! The canvas pane renders the main document viewport with a document tab bar.
|
||
//! Utility panes render their respective panel content.
|
||
//! All panels receive ThemeColors for consistent styling.
|
||
|
||
use crate::app::Message;
|
||
use crate::dock::state::{DockState, PaneType};
|
||
use crate::panels::styles;
|
||
use crate::panels::typography::TITLE;
|
||
use crate::theme::ThemeColors;
|
||
use iced::widget::pane_grid::{Content, Controls, TitleBar};
|
||
use iced::widget::svg;
|
||
use iced::widget::{button, column, container, pane_grid, row, text};
|
||
use iced::{Element, Length};
|
||
|
||
/// Build the dock view element.
|
||
///
|
||
/// Renders a PaneGrid where each pane displays the appropriate panel content
|
||
/// based on its PaneType. Title bars show the pane name with close/maximize buttons.
|
||
/// The canvas pane includes a document tab bar for multi-document support.
|
||
pub fn dock_view<'a>(
|
||
dock: &'a DockState,
|
||
app: &'a crate::app::HcieIcedApp,
|
||
) -> Element<'a, Message> {
|
||
let colors = app.theme_state.colors();
|
||
let custom_drag = dock.drag.is_some() || dock.floating_drag.is_some();
|
||
|
||
let grid: Element<'a, Message> =
|
||
pane_grid::PaneGrid::new(&dock.pane_grid, |pane, pane_type, _is_maximized| {
|
||
// Canvas pane has no title bar (the document tab bar serves as its header).
|
||
// All other panes get a Photoshop-style title bar with close/maximize buttons.
|
||
let title: Option<TitleBar<'a, Message>> = if *pane_type == PaneType::Canvas {
|
||
None
|
||
} else {
|
||
Some(make_title_bar(
|
||
pane,
|
||
*pane_type,
|
||
dock.focused_pane == Some(pane),
|
||
colors,
|
||
))
|
||
};
|
||
|
||
let body = panel_body(*pane_type, app, colors);
|
||
|
||
let mut content = Content::new(body);
|
||
|
||
// Apply per-pane background style. Canvas pane gets a dark workspace
|
||
// background (no border); all other panes get panel background + border
|
||
// so they are visually separated from neighbors.
|
||
if *pane_type == PaneType::Canvas {
|
||
content = content.style(move |_theme| styles::canvas_pane_background(colors));
|
||
} else {
|
||
content = content.style(move |_theme| styles::pane_background(colors));
|
||
}
|
||
|
||
if let Some(title_bar) = title {
|
||
content = content.title_bar(title_bar);
|
||
}
|
||
content
|
||
})
|
||
.width(Length::Fill)
|
||
.height(Length::Fill)
|
||
.spacing(4)
|
||
.on_click(Message::PaneClicked)
|
||
.on_drag(Message::PaneDragged)
|
||
.on_resize(4, Message::PaneResized)
|
||
.style(move |_theme| pane_grid::Style {
|
||
hovered_region: pane_grid::Highlight {
|
||
background: iced::Background::Color(iced::Color::from_rgba(
|
||
colors.accent.r,
|
||
colors.accent.g,
|
||
colors.accent.b,
|
||
if custom_drag { 0.0 } else { 0.15 },
|
||
)),
|
||
border: iced::Border::default()
|
||
.color(colors.accent)
|
||
.width(if custom_drag { 0 } else { 1 }),
|
||
},
|
||
picked_split: pane_grid::Line {
|
||
color: colors.accent,
|
||
width: 2.0,
|
||
},
|
||
hovered_split: pane_grid::Line {
|
||
color: colors.border_high,
|
||
width: 1.0,
|
||
},
|
||
})
|
||
.into();
|
||
|
||
let rail = crate::dock::auto_hide::right_rail(dock.auto_hidden[1].iter().copied(), colors);
|
||
let base: Element<'a, Message> = row![grid, rail]
|
||
.width(Length::Fill)
|
||
.height(Length::Fill)
|
||
.into();
|
||
let base: Element<'a, Message> =
|
||
if let Some(preview) = dock
|
||
.drag
|
||
.as_ref()
|
||
.and_then(|drag| drag.accepted_preview)
|
||
.or_else(|| dock.floating_target.map(|target| target.preview))
|
||
{
|
||
iced::widget::Stack::new()
|
||
.push(base)
|
||
.push(preview_overlay(preview, colors))
|
||
.width(Length::Fill)
|
||
.height(Length::Fill)
|
||
.into()
|
||
} else {
|
||
base
|
||
};
|
||
if let Some(panel) = dock.active_auto_hide {
|
||
let overlay =
|
||
crate::dock::auto_hide::overlay(panel, panel_body(panel, app, colors), colors);
|
||
iced::widget::Stack::new()
|
||
.push(base)
|
||
.push(overlay)
|
||
.width(Length::Fill)
|
||
.height(Length::Fill)
|
||
.into()
|
||
} else {
|
||
base
|
||
}
|
||
}
|
||
|
||
/// Builds reusable panel content for docked and temporary auto-hide placements.
|
||
///
|
||
/// Canvas remains exclusive to PaneGrid; callers must never request it for detached placement.
|
||
pub fn panel_body<'a>(
|
||
pane_type: PaneType,
|
||
app: &'a crate::app::HcieIcedApp,
|
||
colors: ThemeColors,
|
||
) -> Element<'a, Message> {
|
||
match &pane_type {
|
||
PaneType::Canvas => {
|
||
if app.show_welcome {
|
||
return crate::dock::welcome::view(colors);
|
||
}
|
||
// Canvas pane includes document tab bar as its header
|
||
let doc_tab_bar = document_tab_bar(app, colors);
|
||
let canvas = {
|
||
let doc = &app.documents[app.active_doc];
|
||
let pressure = app
|
||
.tablet_state
|
||
.lock()
|
||
.map(|s| s.current_pressure())
|
||
.unwrap_or(1.0);
|
||
let ts = &app.settings.tool_settings;
|
||
crate::canvas::view(
|
||
doc,
|
||
&app.tool_state,
|
||
app.marching_ants_offset,
|
||
pressure,
|
||
ts.vector_points,
|
||
ts.vector_sides,
|
||
ts.vector_radius,
|
||
)
|
||
};
|
||
column![doc_tab_bar, canvas]
|
||
.width(Length::Fill)
|
||
.height(Length::Fill)
|
||
.into()
|
||
}
|
||
// Tools is no longer in the dock — toolbox is a fixed strip on the left
|
||
PaneType::Layers => {
|
||
let doc = &app.documents[app.active_doc];
|
||
crate::panels::layers::view(
|
||
&doc.cached_layers,
|
||
doc.engine.active_layer_id(),
|
||
&doc.engine,
|
||
colors,
|
||
)
|
||
}
|
||
PaneType::History => {
|
||
let doc = &app.documents[app.active_doc];
|
||
crate::panels::history::view(&doc.cached_history, doc.history_current, colors)
|
||
}
|
||
PaneType::Brushes => crate::panels::brushes::view(
|
||
app.tool_state.brush_style,
|
||
app.tool_state.brush_size,
|
||
app.tool_state.brush_opacity,
|
||
app.tool_state.brush_hardness,
|
||
colors,
|
||
),
|
||
PaneType::Filters => crate::panels::filters::view(
|
||
app.selected_filter,
|
||
&app.filter_params,
|
||
app.filter_preview_active,
|
||
app.documents[app.active_doc]
|
||
.engine
|
||
.active_layer_is_editable(),
|
||
colors,
|
||
),
|
||
PaneType::ColorPicker => crate::color_picker::view(
|
||
&app.fg_color,
|
||
&app.bg_color,
|
||
&app.recent_colors,
|
||
app.color_tab,
|
||
),
|
||
PaneType::Properties => {
|
||
let doc = &app.documents[app.active_doc];
|
||
let active_id = doc.engine.active_layer_id();
|
||
let layer_info = doc.cached_layers.iter().find(|l| l.id == active_id);
|
||
let layer_name = layer_info.map(|l| l.name.as_str()).unwrap_or("—");
|
||
let layer_opacity = layer_info.map(|l| l.opacity).unwrap_or(1.0);
|
||
let layer_visible = layer_info.map(|l| l.visible).unwrap_or(true);
|
||
let layer_blend_mode = layer_info
|
||
.map(|l| l.blend_mode)
|
||
.unwrap_or(hcie_engine_api::BlendMode::Normal);
|
||
let layer_type = layer_info
|
||
.map(|l| l.layer_type)
|
||
.unwrap_or(hcie_engine_api::LayerType::Raster);
|
||
let layer_width = layer_info.map(|l| l.width).unwrap_or(0);
|
||
let layer_height = layer_info.map(|l| l.height).unwrap_or(0);
|
||
let vector_shapes = doc.engine.active_vector_shapes().unwrap_or_default();
|
||
let ts2 = app.settings.tool_settings.clone();
|
||
crate::panels::properties::view(
|
||
&app.tool_state.active_tool,
|
||
app.tool_state.brush_size,
|
||
app.tool_state.brush_opacity,
|
||
app.tool_state.brush_hardness,
|
||
doc.selection_rect,
|
||
doc.vector_draw,
|
||
colors,
|
||
layer_name,
|
||
layer_opacity,
|
||
layer_visible,
|
||
layer_blend_mode,
|
||
layer_type,
|
||
layer_width,
|
||
layer_height,
|
||
doc.selected_vector_shape,
|
||
&vector_shapes,
|
||
ts2.vector_stroke_width,
|
||
ts2.vector_opacity,
|
||
ts2.vector_fill,
|
||
ts2.vector_radius,
|
||
ts2.vector_points,
|
||
ts2.vector_sides,
|
||
active_id,
|
||
app.fg_color,
|
||
app.bg_color,
|
||
)
|
||
}
|
||
PaneType::Script => crate::panels::script_panel::view(
|
||
&app.script_content,
|
||
app.script_error.as_deref(),
|
||
app.script_error_line,
|
||
app.script_is_running,
|
||
colors,
|
||
),
|
||
PaneType::AiChat => crate::panels::ai_chat_panel::view(&app.ai_chat_state, colors),
|
||
PaneType::AiScript => crate::panels::ai_script_panel::view(app),
|
||
PaneType::LayerDetails => {
|
||
let doc = &app.documents[app.active_doc];
|
||
crate::panels::layer_details::view(
|
||
&doc.cached_layers,
|
||
doc.engine.active_layer_id(),
|
||
colors,
|
||
)
|
||
}
|
||
PaneType::Geometry => {
|
||
let shapes = app.documents[app.active_doc]
|
||
.engine
|
||
.active_vector_shapes()
|
||
.unwrap_or_default();
|
||
crate::panels::geometry::view(
|
||
shapes,
|
||
colors,
|
||
app.documents[app.active_doc].selected_vector_shape,
|
||
app.bool_shape_a,
|
||
app.bool_shape_b,
|
||
)
|
||
}
|
||
PaneType::ToolSettings => {
|
||
let fg = app.fg_color;
|
||
let draft = app.documents[app.active_doc].text_draft.as_ref();
|
||
crate::panels::tool_settings::view(&app.tool_state, &app.settings, fg, draft, colors)
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Build the document tab bar.
|
||
///
|
||
/// Shows a horizontal tab for each open document. The active document tab
|
||
/// is highlighted. Each tab shows the document name and a close button.
|
||
/// Modified documents show an asterisk (*). Tabs have hover feedback.
|
||
fn document_tab_bar<'a>(
|
||
app: &'a crate::app::HcieIcedApp,
|
||
colors: ThemeColors,
|
||
) -> Element<'a, Message> {
|
||
let mut tabs = row![].spacing(0);
|
||
|
||
for (idx, doc) in app.documents.iter().enumerate() {
|
||
let is_active = idx == app.active_doc;
|
||
let modified_indicator = if doc.modified { " *" } else { "" };
|
||
let label = format!("{}{}", doc.name, modified_indicator);
|
||
|
||
let tab_text = text(label).size(11);
|
||
let tab_text = if is_active {
|
||
tab_text.style(move |_theme: &iced::Theme| iced::widget::text::Style {
|
||
color: Some(colors.accent),
|
||
})
|
||
} else {
|
||
tab_text.style(move |_theme: &iced::Theme| iced::widget::text::Style {
|
||
color: Some(colors.text_primary),
|
||
})
|
||
};
|
||
|
||
let active = is_active;
|
||
let c = colors;
|
||
|
||
let tab = button(
|
||
row![
|
||
tab_text,
|
||
iced::widget::tooltip(
|
||
button(text("×").size(10))
|
||
.on_press(Message::DocClose(idx))
|
||
.padding([0, 4])
|
||
.style(
|
||
move |_theme: &iced::Theme, status: iced::widget::button::Status| {
|
||
match status {
|
||
iced::widget::button::Status::Hovered => {
|
||
iced::widget::button::Style {
|
||
background: Some(iced::Background::Color(c.bg_active)),
|
||
text_color: c.text_primary,
|
||
border: iced::Border::default(),
|
||
..Default::default()
|
||
}
|
||
}
|
||
_ => iced::widget::button::Style {
|
||
background: Some(iced::Background::Color(
|
||
iced::Color::from_rgba(0.0, 0.0, 0.0, 0.0),
|
||
)),
|
||
text_color: c.text_secondary,
|
||
border: iced::Border::default(),
|
||
..Default::default()
|
||
},
|
||
}
|
||
}
|
||
),
|
||
text(format!("Close {}", doc.name)).size(11),
|
||
iced::widget::tooltip::Position::Bottom,
|
||
),
|
||
]
|
||
.spacing(4)
|
||
.align_y(iced::Alignment::Center),
|
||
)
|
||
.on_press(Message::DocSwitch(idx))
|
||
.padding([4, 8])
|
||
.style(
|
||
move |_theme: &iced::Theme, status: iced::widget::button::Status| match status {
|
||
iced::widget::button::Status::Active => {
|
||
if active {
|
||
iced::widget::button::Style {
|
||
background: Some(iced::Background::Color(c.bg_active)),
|
||
border: iced::Border::default().color(c.accent).width(1),
|
||
text_color: c.accent,
|
||
..Default::default()
|
||
}
|
||
} else {
|
||
iced::widget::button::Style {
|
||
background: Some(iced::Background::Color(c.bg_panel)),
|
||
border: iced::Border::default().color(c.border_low).width(1),
|
||
text_color: c.text_primary,
|
||
..Default::default()
|
||
}
|
||
}
|
||
}
|
||
iced::widget::button::Status::Hovered => {
|
||
if active {
|
||
iced::widget::button::Style {
|
||
background: Some(iced::Background::Color(c.bg_active)),
|
||
border: iced::Border::default().color(c.accent).width(1),
|
||
text_color: c.accent,
|
||
..Default::default()
|
||
}
|
||
} else {
|
||
iced::widget::button::Style {
|
||
background: Some(iced::Background::Color(c.bg_hover)),
|
||
border: iced::Border::default().color(c.border_high).width(1),
|
||
text_color: c.text_primary,
|
||
..Default::default()
|
||
}
|
||
}
|
||
}
|
||
_ => iced::widget::button::Style {
|
||
background: Some(iced::Background::Color(c.bg_active)),
|
||
border: iced::Border::default().color(c.accent).width(1),
|
||
text_color: c.accent,
|
||
..Default::default()
|
||
},
|
||
},
|
||
);
|
||
|
||
tabs = tabs.push(tab);
|
||
}
|
||
|
||
// Add new document button with hover
|
||
let new_btn = button(text("+").size(12))
|
||
.on_press(Message::DialogOpen(crate::app::ActiveDialog::NewImage))
|
||
.padding([4, 8])
|
||
.style(
|
||
move |_theme: &iced::Theme, status: iced::widget::button::Status| match status {
|
||
iced::widget::button::Status::Hovered => iced::widget::button::Style {
|
||
background: Some(iced::Background::Color(colors.bg_hover)),
|
||
text_color: colors.text_primary,
|
||
border: iced::Border::default(),
|
||
..Default::default()
|
||
},
|
||
_ => iced::widget::button::Style {
|
||
background: Some(iced::Background::Color(iced::Color::from_rgba(
|
||
0.0, 0.0, 0.0, 0.0,
|
||
))),
|
||
text_color: colors.text_secondary,
|
||
border: iced::Border::default(),
|
||
..Default::default()
|
||
},
|
||
},
|
||
);
|
||
|
||
tabs = tabs.push(new_btn);
|
||
|
||
container(tabs)
|
||
.width(Length::Fill)
|
||
.style(move |_theme| styles::tabbar_background(colors))
|
||
.into()
|
||
}
|
||
|
||
/// Map pane type to its SVG icon path.
|
||
fn pane_type_icon(pane_type: PaneType) -> Option<&'static str> {
|
||
match pane_type {
|
||
PaneType::Canvas => None,
|
||
PaneType::Layers => Some("icons/panel-layers.svg"),
|
||
PaneType::History => Some("icons/panel-history.svg"),
|
||
PaneType::Brushes => Some("icons/panel-brush.svg"),
|
||
PaneType::Filters => Some("icons/panel-filters.svg"),
|
||
PaneType::ColorPicker => Some("icons/panel-colorbox.svg"),
|
||
PaneType::Properties => Some("icons/panel-settings.svg"),
|
||
PaneType::Script => Some("icons/panel-pen.svg"),
|
||
PaneType::AiChat => Some("icons/panel-ai.svg"),
|
||
PaneType::LayerDetails => Some("icons/panel-layers.svg"),
|
||
PaneType::Geometry => Some("icons/vector_rect.svg"),
|
||
PaneType::ToolSettings => Some("icons/panel-settings.svg"),
|
||
PaneType::AiScript => Some("icons/panel-ai.svg"),
|
||
}
|
||
}
|
||
|
||
/// Creates a compact VS-style title bar whose controls do not initiate pane dragging.
|
||
fn make_title_bar<'a>(
|
||
pane: pane_grid::Pane,
|
||
pane_type: PaneType,
|
||
focused: bool,
|
||
colors: ThemeColors,
|
||
) -> TitleBar<'a, Message> {
|
||
let icon_paths = [
|
||
std::path::PathBuf::from("hcie-iced-app/assets"),
|
||
std::path::PathBuf::from("/mnt/extra/00_PROJECTS/hcie-rust-v3.05/hcie-iced-app/assets"),
|
||
];
|
||
let title_content: Element<'_, Message> = if let Some(icon_file) = pane_type_icon(pane_type) {
|
||
if let Some(base) = icon_paths.iter().find(|path| path.join(icon_file).exists()) {
|
||
let icon = svg(svg::Handle::from_path(base.join(icon_file)))
|
||
.width(14)
|
||
.height(14)
|
||
.style(move |_theme, _status| svg::Style {
|
||
color: Some(if focused {
|
||
colors.accent
|
||
} else {
|
||
colors.text_secondary
|
||
}),
|
||
});
|
||
row![
|
||
icon,
|
||
text(pane_type.label()).size(TITLE).color(if focused {
|
||
colors.text_primary
|
||
} else {
|
||
colors.text_secondary
|
||
})
|
||
]
|
||
.spacing(6)
|
||
.align_y(iced::Alignment::Center)
|
||
.into()
|
||
} else {
|
||
text(pane_type.label())
|
||
.size(TITLE)
|
||
.color(colors.text_secondary)
|
||
.into()
|
||
}
|
||
} else {
|
||
text(pane_type.label())
|
||
.size(TITLE)
|
||
.color(colors.text_secondary)
|
||
.into()
|
||
};
|
||
|
||
let control = |label, message| {
|
||
button(text(label).size(12))
|
||
.on_press(message)
|
||
.padding([3, 6])
|
||
.style(move |_theme, status| chrome_button_style(colors, status))
|
||
};
|
||
let pin = iced::widget::tooltip(
|
||
control("−", Message::PaneAutoHide(pane)),
|
||
text("Minimize to side rail").size(11),
|
||
iced::widget::tooltip::Position::Bottom,
|
||
);
|
||
let float_control = iced::widget::tooltip(
|
||
control("↗", Message::PaneFloat(pane)),
|
||
text("Float panel").size(11),
|
||
iced::widget::tooltip::Position::Bottom,
|
||
);
|
||
let maximize = iced::widget::tooltip(
|
||
control("□", Message::PaneMaximize(pane)),
|
||
text("Maximize / restore").size(11),
|
||
iced::widget::tooltip::Position::Bottom,
|
||
);
|
||
let close = iced::widget::tooltip(
|
||
control("×", Message::PaneClose(pane)),
|
||
text("Close panel").size(11),
|
||
iced::widget::tooltip::Position::Bottom,
|
||
);
|
||
let compact_control = |label, message| {
|
||
button(text(label).size(11))
|
||
.on_press(message)
|
||
.padding([3, 3])
|
||
.style(move |_theme, status| chrome_button_style(colors, status))
|
||
};
|
||
let compact_pin = iced::widget::tooltip(
|
||
compact_control("−", Message::PaneAutoHide(pane)),
|
||
text("Minimize to side rail").size(11),
|
||
iced::widget::tooltip::Position::Bottom,
|
||
);
|
||
let compact_float = iced::widget::tooltip(
|
||
compact_control("↗", Message::PaneFloat(pane)),
|
||
text("Float panel").size(11),
|
||
iced::widget::tooltip::Position::Bottom,
|
||
);
|
||
let compact_maximize = iced::widget::tooltip(
|
||
compact_control("□", Message::PaneMaximize(pane)),
|
||
text("Maximize / restore").size(11),
|
||
iced::widget::tooltip::Position::Bottom,
|
||
);
|
||
let compact_close = iced::widget::tooltip(
|
||
compact_control("×", Message::PaneClose(pane)),
|
||
text("Close panel").size(11),
|
||
iced::widget::tooltip::Position::Bottom,
|
||
);
|
||
let full_controls = row![pin, float_control, maximize, close]
|
||
.spacing(1)
|
||
.align_y(iced::Alignment::Center)
|
||
.height(28);
|
||
let compact_controls = row![compact_pin, compact_float, compact_maximize, compact_close]
|
||
.spacing(1)
|
||
.align_y(iced::Alignment::Center)
|
||
.height(28);
|
||
let title_drag = iced::widget::mouse_area(title_content)
|
||
.on_press(Message::DockHeaderDragStart(pane))
|
||
.interaction(iced::mouse::Interaction::Grab);
|
||
let header_colors = colors;
|
||
TitleBar::new(
|
||
row![title_drag]
|
||
.spacing(1)
|
||
.align_y(iced::Alignment::Center)
|
||
.height(28),
|
||
)
|
||
.controls(Controls::dynamic(full_controls, compact_controls))
|
||
.always_show_controls()
|
||
.padding([0, 6])
|
||
.style(move |_theme| styles::modern_panel_header(header_colors, focused))
|
||
}
|
||
|
||
/// Shared hover and pressed style for dock chrome and auto-hide controls.
|
||
pub fn chrome_button_style(
|
||
colors: ThemeColors,
|
||
status: iced::widget::button::Status,
|
||
) -> iced::widget::button::Style {
|
||
let background = match status {
|
||
iced::widget::button::Status::Hovered => Some(iced::Background::Color(colors.bg_hover)),
|
||
iced::widget::button::Status::Pressed => Some(iced::Background::Color(colors.bg_active)),
|
||
_ => None,
|
||
};
|
||
iced::widget::button::Style {
|
||
background,
|
||
text_color: colors.text_secondary,
|
||
border: iced::Border {
|
||
radius: 2.0.into(),
|
||
..Default::default()
|
||
},
|
||
..Default::default()
|
||
}
|
||
}
|
||
|
||
/// Renders a translucent accepted rectangle and compact edge compass without capturing events.
|
||
fn preview_overlay<'a>(rect: iced::Rectangle, colors: ThemeColors) -> Element<'a, Message> {
|
||
let fill = container(text(""))
|
||
.width(rect.width)
|
||
.height(rect.height)
|
||
.style(move |_theme| iced::widget::container::Style {
|
||
background: Some(iced::Background::Color(iced::Color::from_rgba(
|
||
colors.accent.r,
|
||
colors.accent.g,
|
||
colors.accent.b,
|
||
0.22,
|
||
))),
|
||
border: iced::Border::default().color(colors.accent).width(2),
|
||
..Default::default()
|
||
});
|
||
let marker = |glyph, active| {
|
||
container(text(glyph).size(12).color(if active {
|
||
colors.text_primary
|
||
} else {
|
||
colors.text_disabled
|
||
}))
|
||
.center_x(24)
|
||
.center_y(24)
|
||
.style(move |_theme| iced::widget::container::Style {
|
||
background: Some(iced::Background::Color(if active {
|
||
colors.accent
|
||
} else {
|
||
colors.bg_app
|
||
})),
|
||
border: iced::Border::default().color(colors.border_high).width(1),
|
||
..Default::default()
|
||
})
|
||
};
|
||
let compass = column![
|
||
row![
|
||
iced::widget::Space::with_width(24),
|
||
marker("↑", true),
|
||
iced::widget::Space::with_width(24)
|
||
],
|
||
row![marker("←", true), marker("×", false), marker("→", true)],
|
||
row![
|
||
iced::widget::Space::with_width(24),
|
||
marker("↓", true),
|
||
iced::widget::Space::with_width(24)
|
||
],
|
||
]
|
||
.spacing(1);
|
||
let compass_x = (rect.center_x() - 36.0).max(0.0);
|
||
let compass_y = (rect.center_y() - 36.0).max(0.0);
|
||
iced::widget::Stack::new()
|
||
.push(column![
|
||
iced::widget::Space::with_height(rect.y),
|
||
row![iced::widget::Space::with_width(rect.x), fill]
|
||
])
|
||
.push(column![
|
||
iced::widget::Space::with_height(compass_y),
|
||
row![iced::widget::Space::with_width(compass_x), compass]
|
||
])
|
||
.width(Length::Fill)
|
||
.height(Length::Fill)
|
||
.into()
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
/// Guards the Iced drag contract: title and controls must be distinct widget regions.
|
||
#[test]
|
||
fn title_bar_uses_explicit_drag_handle_and_separate_controls() {
|
||
let source = include_str!("view.rs");
|
||
let start = source.find("fn make_title_bar").unwrap();
|
||
let end = source[start..].find("pub fn chrome_button_style").unwrap() + start;
|
||
let title_source = &source[start..end];
|
||
assert!(title_source.contains("Controls::dynamic"));
|
||
assert!(title_source.contains(".controls("));
|
||
assert!(title_source.contains(".always_show_controls()"));
|
||
assert!(title_source.contains("mouse_area(title_content)"));
|
||
assert!(title_source.contains("DockHeaderDragStart(pane)"));
|
||
assert!(!title_source.contains("Space::with_width(Length::Fill)"));
|
||
}
|
||
}
|