Files
hcie-rust-v3.05/hcie-iced-app/crates/hcie-iced-gui/src/dock/view.rs
T

436 lines
19 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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 iced::widget::svg;
use crate::theme::ThemeColors;
use iced::widget::pane_grid::{Content, TitleBar};
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();
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, colors))
};
let body: Element<'a, Message> = match pane_type {
PaneType::Canvas => {
// 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, 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,
)
}
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)
}
};
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, 0.15,
)),
border: iced::Border::default().color(colors.accent).width(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()
}
/// 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,
button(text("×").size(10))
.on_press(Message::NoOp)
.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()
},
}
}),
]
.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"),
}
}
/// Create a Photoshop-style title bar for a pane.
///
/// Shows a panel type icon + label with close and maximize buttons.
/// Uses `panel_header` style from the theme for consistent header appearance.
/// Buttons have hover feedback: transparent by default, bg_hover on hover.
fn make_title_bar<'a>(pane: pane_grid::Pane, pane_type: PaneType, colors: ThemeColors) -> TitleBar<'a, Message> {
// Panel type icon
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(|p| p.join(icon_file).exists()) {
let handle = svg::Handle::from_path(base.join(icon_file));
let icon = svg(handle)
.width(14)
.height(14)
.style(move |_theme: &iced::Theme, _status: iced::widget::svg::Status| svg::Style {
color: Some(colors.text_secondary),
});
row![icon, text(pane_type.label()).size(11).color(colors.text_secondary)]
.spacing(4)
.align_y(iced::Alignment::Center)
.into()
} else {
text(pane_type.label()).size(11).color(colors.text_secondary).into()
}
} else {
text(pane_type.label()).size(11).color(colors.text_secondary).into()
};
// Close button with hover state
let close_btn = button(text("×").size(11))
.on_press(Message::PaneClose(pane))
.padding([2, 6])
.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::Status::Pressed => iced::widget::button::Style {
background: Some(iced::Background::Color(colors.bg_active)),
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()
},
}
});
// Maximize button with hover state
let maximize_btn = button(text("").size(11))
.on_press(Message::PaneMaximize(pane))
.padding([2, 6])
.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::Status::Pressed => iced::widget::button::Style {
background: Some(iced::Background::Color(colors.bg_active)),
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()
},
}
});
TitleBar::new(
row![title_content, iced::widget::Space::with_width(Length::Fill), maximize_btn, close_btn]
.spacing(2)
.align_y(iced::Alignment::Center)
)
.style(move |_theme| styles::panel_header(colors))
}