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

436 lines
19 KiB
Rust
Raw Normal View History

//! 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];
2026-07-13 06:40:14 +03:00
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 => {
2026-07-15 16:17:43 +03:00
crate::panels::filters::view(app.selected_filter, &app.filter_params, app.filter_preview_active, colors)
}
PaneType::ColorPicker => {
2026-07-13 06:40:14 +03:00
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))
}