//! 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> = 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)) }