forked from 1b-agent/opencadstudio-upstream
Each item below was gated by #[allow(dead_code)] and has zero callers across src/ — none of them are part of any public API consumed by external crates (this is a binary, not a library): * modules/insert/underlay_frames.rs + edit_aliases.rs — toolbar stubs whose `tool()` registrars were never wired into the ribbon; the parent mod.rs inlines its own include_bytes! for the SVGs, so the files were orphan dead weight. * ui/snap_popup.rs:snap_marker_color — never called. * ui/ribbon/mod.rs:last_dropdown_cmd — never called. * ui/scale_popup.rs:anno_scale_for_label — never called. * ui/layers.rs:color_name_to_aci — inverse of aci_color_display with zero call sites. * linetypes.rs:name_list — never called. * scene/pipeline/wire_gpu.rs:new_ghost — never called. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1034 lines
37 KiB
Rust
1034 lines
37 KiB
Rust
// Ribbon — tab bar + 3-row tool area.
|
|
//
|
|
// Button sizes:
|
|
// LargeTool / LargeDropdown — full ribbon height (3 rows), icon + label [+ ▾]
|
|
// Tool / Dropdown — 1-row height, icon only [+ ▾ on right]
|
|
//
|
|
// Dropdown items within a group are collected into columns of 3 rows.
|
|
|
|
use std::collections::HashMap;
|
|
|
|
use acadrust::types::{Color as AcadColor, LineWeight};
|
|
use iced::widget::{button, column, container, mouse_area, row, scrollable, svg, text};
|
|
use iced::{Background, Border, Color, Element, Fill, Length, Padding, Theme};
|
|
|
|
use crate::app::Message;
|
|
use crate::modules::registry;
|
|
use crate::modules::{CadModule, IconKind, RibbonItem};
|
|
use crate::ui::properties::{color_picker_dropdown, lw_options, LinetypeItem};
|
|
|
|
mod widgets;
|
|
use widgets::{StyleContext, *};
|
|
|
|
// ── Ribbon state ───────────────────────────────────────────────────────────
|
|
|
|
pub struct Ribbon {
|
|
modules: Vec<Box<dyn CadModule>>,
|
|
active: usize,
|
|
active_tool: Option<String>,
|
|
pub wireframe: bool,
|
|
pub ortho_mode: bool,
|
|
pub open_dropdown: Option<String>,
|
|
last_cmd: HashMap<&'static str, &'static str>,
|
|
pub layer_names: Vec<String>,
|
|
pub active_layer: String,
|
|
pub layer_infos: Vec<LayerInfo>,
|
|
/// Active object color — ACI / ByLayer / ByBlock.
|
|
pub active_color: AcadColor,
|
|
/// Active linetype override ("ByLayer", "Continuous", …).
|
|
pub active_linetype: String,
|
|
/// Active lineweight.
|
|
pub active_lineweight: LineWeight,
|
|
/// Linetypes loaded from the current document (with ASCII art).
|
|
pub available_linetypes: Vec<LinetypeItem>,
|
|
/// Whether the full ACI palette is expanded inside the color picker overlay.
|
|
pub prop_color_palette_open: bool,
|
|
// ── Style selector state ──────────────────────────────────────────────
|
|
pub text_style_names: Vec<String>,
|
|
pub active_text_style: String,
|
|
pub dim_style_names: Vec<String>,
|
|
pub active_dim_style: String,
|
|
pub mleader_style_names: Vec<String>,
|
|
pub active_mleader_style: String,
|
|
pub table_style_names: Vec<String>,
|
|
pub active_table_style: String,
|
|
}
|
|
|
|
/// Per-layer display data shown in the ribbon layer dropdown.
|
|
#[derive(Clone, Debug)]
|
|
pub struct LayerInfo {
|
|
pub name: String,
|
|
pub color: Color,
|
|
pub visible: bool,
|
|
pub frozen: bool,
|
|
pub locked: bool,
|
|
}
|
|
|
|
impl Ribbon {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
modules: registry::all_modules(),
|
|
active: 0,
|
|
active_tool: None,
|
|
wireframe: false,
|
|
ortho_mode: true,
|
|
open_dropdown: None,
|
|
last_cmd: HashMap::new(),
|
|
layer_names: vec!["0".to_string()],
|
|
active_layer: "0".to_string(),
|
|
layer_infos: vec![LayerInfo {
|
|
name: "0".to_string(),
|
|
color: Color::WHITE,
|
|
visible: true,
|
|
frozen: false,
|
|
locked: false,
|
|
}],
|
|
active_color: AcadColor::ByLayer,
|
|
active_linetype: "ByLayer".to_string(),
|
|
active_lineweight: LineWeight::ByLayer,
|
|
available_linetypes: vec![LinetypeItem {
|
|
name: "Continuous".to_string(),
|
|
art: String::new(),
|
|
}],
|
|
prop_color_palette_open: false,
|
|
text_style_names: vec!["Standard".to_string()],
|
|
active_text_style: "Standard".to_string(),
|
|
dim_style_names: vec!["Standard".to_string()],
|
|
active_dim_style: "Standard".to_string(),
|
|
mleader_style_names: vec!["Standard".to_string()],
|
|
active_mleader_style: "Standard".to_string(),
|
|
table_style_names: vec!["Standard".to_string()],
|
|
active_table_style: "Standard".to_string(),
|
|
}
|
|
}
|
|
|
|
pub fn set_styles(
|
|
&mut self,
|
|
text: Vec<String>,
|
|
active_text: &str,
|
|
dim: Vec<String>,
|
|
active_dim: &str,
|
|
mleader: Vec<String>,
|
|
active_mleader: &str,
|
|
table: Vec<String>,
|
|
active_table: &str,
|
|
) {
|
|
self.text_style_names = text;
|
|
self.active_text_style = active_text.to_string();
|
|
self.dim_style_names = dim;
|
|
self.active_dim_style = active_dim.to_string();
|
|
self.mleader_style_names = mleader;
|
|
self.active_mleader_style = active_mleader.to_string();
|
|
self.table_style_names = table;
|
|
self.active_table_style = active_table.to_string();
|
|
}
|
|
|
|
pub fn set_layers(&mut self, infos: Vec<LayerInfo>, active: &str) {
|
|
self.active_layer = active.to_string();
|
|
self.layer_names = infos.iter().map(|l| l.name.clone()).collect();
|
|
self.layer_infos = infos;
|
|
}
|
|
|
|
pub fn set_available_linetypes(&mut self, items: Vec<LinetypeItem>) {
|
|
self.available_linetypes = items;
|
|
}
|
|
|
|
pub fn select(&mut self, index: usize) {
|
|
if index < self.modules.len() {
|
|
self.active = index;
|
|
}
|
|
}
|
|
pub fn activate_tool(&mut self, id: &str) {
|
|
self.active_tool = Some(id.to_string());
|
|
}
|
|
pub fn deactivate_tool(&mut self) {
|
|
self.active_tool = None;
|
|
}
|
|
pub fn set_wireframe(&mut self, w: bool) {
|
|
self.wireframe = w;
|
|
}
|
|
pub fn set_ortho(&mut self, ortho: bool) {
|
|
self.ortho_mode = ortho;
|
|
}
|
|
|
|
pub fn toggle_dropdown(&mut self, id: &str) {
|
|
if self.open_dropdown.as_deref() == Some(id) {
|
|
self.open_dropdown = None;
|
|
} else {
|
|
self.open_dropdown = Some(id.to_string());
|
|
}
|
|
}
|
|
pub fn close_dropdown(&mut self) {
|
|
self.open_dropdown = None;
|
|
}
|
|
|
|
/// Returns the index of the Layout module in the modules list.
|
|
pub fn layout_module_index(&self) -> Option<usize> {
|
|
self.modules.iter().position(|m| m.id() == "layout")
|
|
}
|
|
|
|
/// Returns true if the currently active tab is the Layout module.
|
|
pub fn active_is_layout(&self) -> bool {
|
|
self.modules
|
|
.get(self.active)
|
|
.map(|m| m.id() == "layout")
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
pub fn select_dropdown_item(&mut self, dropdown_id: &'static str, cmd: &'static str) {
|
|
self.last_cmd.insert(dropdown_id, cmd);
|
|
self.open_dropdown = None;
|
|
}
|
|
|
|
// ── View ──────────────────────────────────────────────────────────────
|
|
|
|
pub fn view(
|
|
&self,
|
|
is_paper: bool,
|
|
undo_count: usize,
|
|
redo_count: usize,
|
|
) -> Element<'_, Message> {
|
|
// ── Logo ──────────────────────────────────────────────────────────
|
|
let logo_svg = {
|
|
let handle = svg::Handle::from_memory(include_bytes!("../../../assets/logo.svg"));
|
|
svg(handle).width(30).height(28)
|
|
};
|
|
let logo = button(logo_svg)
|
|
.on_press(Message::ToggleAppMenu)
|
|
.style(|_: &Theme, status| button::Style {
|
|
background: Some(Background::Color(match status {
|
|
button::Status::Hovered | button::Status::Pressed => Color {
|
|
r: 0.80,
|
|
g: 0.25,
|
|
b: 0.15,
|
|
a: 1.0,
|
|
},
|
|
_ => LOGO_RED,
|
|
})),
|
|
border: Border {
|
|
radius: 2.0.into(),
|
|
..Default::default()
|
|
},
|
|
shadow: iced::Shadow::default(),
|
|
snap: false,
|
|
..Default::default()
|
|
})
|
|
.padding([0, 4]);
|
|
|
|
// ── Tab buttons ───────────────────────────────────────────────────
|
|
let history_controls = row![
|
|
render_history_control(
|
|
"↶",
|
|
"Undo",
|
|
UNDO_HISTORY_ID,
|
|
undo_count,
|
|
&self.open_dropdown
|
|
),
|
|
render_history_control(
|
|
"↷",
|
|
"Redo",
|
|
REDO_HISTORY_ID,
|
|
redo_count,
|
|
&self.open_dropdown
|
|
),
|
|
]
|
|
.spacing(TOP_HIST_GAP)
|
|
.align_y(iced::Center);
|
|
|
|
let tab_buttons = self.modules.iter().enumerate().fold(
|
|
row![logo, history_controls]
|
|
.align_y(iced::Center)
|
|
.spacing(6),
|
|
|row_acc, (i, module)| {
|
|
if module.id() == "layout" && !is_paper {
|
|
return row_acc;
|
|
}
|
|
|
|
let is_active = i == self.active;
|
|
let is_contextual = module.id() == "layout";
|
|
let accent = if is_contextual {
|
|
ACCENT_GOLD
|
|
} else {
|
|
ACCENT_BLUE
|
|
};
|
|
let text_inactive = if is_contextual {
|
|
Color {
|
|
r: 0.90,
|
|
g: 0.72,
|
|
b: 0.30,
|
|
a: 1.0,
|
|
}
|
|
} else {
|
|
Color {
|
|
r: 0.75,
|
|
g: 0.75,
|
|
b: 0.75,
|
|
a: 1.0,
|
|
}
|
|
};
|
|
let hover_bg = if is_contextual {
|
|
Color {
|
|
r: 0.28,
|
|
g: 0.24,
|
|
b: 0.12,
|
|
a: 1.0,
|
|
}
|
|
} else {
|
|
Color {
|
|
r: 0.25,
|
|
g: 0.25,
|
|
b: 0.25,
|
|
a: 1.0,
|
|
}
|
|
};
|
|
let btn = container(
|
|
button(text(module.title()).size(12))
|
|
.on_press(Message::RibbonSelectTab(i))
|
|
.style(move |_: &Theme, status| button::Style {
|
|
background: Some(Background::Color(match (is_active, status) {
|
|
(true, _) => RIBBON_BG,
|
|
(false, button::Status::Hovered) => hover_bg,
|
|
_ => Color::TRANSPARENT,
|
|
})),
|
|
text_color: if is_active {
|
|
Color::WHITE
|
|
} else {
|
|
text_inactive
|
|
},
|
|
border: Border {
|
|
color: if is_active {
|
|
accent
|
|
} else {
|
|
Color::TRANSPARENT
|
|
},
|
|
width: if is_active { 2.0 } else { 0.0 },
|
|
radius: 0.0.into(),
|
|
},
|
|
shadow: iced::Shadow::default(),
|
|
snap: false,
|
|
})
|
|
.padding([5, 14]),
|
|
)
|
|
.style(move |_: &Theme| container::Style {
|
|
border: Border {
|
|
color: if is_active {
|
|
accent
|
|
} else {
|
|
Color::TRANSPARENT
|
|
},
|
|
width: if is_active { 2.0 } else { 0.0 },
|
|
radius: 0.0.into(),
|
|
},
|
|
..Default::default()
|
|
});
|
|
row_acc.push(btn)
|
|
},
|
|
);
|
|
|
|
let tab_bar = container(tab_buttons)
|
|
.style(|_: &Theme| container::Style {
|
|
background: Some(Background::Color(TOPBAR_BG)),
|
|
..Default::default()
|
|
})
|
|
.width(Length::Fill)
|
|
.height(28);
|
|
|
|
// ── Tool area ─────────────────────────────────────────────────────
|
|
let effective_active = if !is_paper
|
|
&& self
|
|
.modules
|
|
.get(self.active)
|
|
.map(|m| m.id() == "layout")
|
|
.unwrap_or(false)
|
|
{
|
|
0
|
|
} else {
|
|
self.active
|
|
};
|
|
let tool_area: Element<'_, Message> =
|
|
if let Some(module) = self.modules.get(effective_active) {
|
|
let groups = module.ribbon_groups();
|
|
let wireframe = self.wireframe;
|
|
let ortho_mode = self.ortho_mode;
|
|
let active_tool = self.active_tool.clone();
|
|
let open_dd = self.open_dropdown.clone();
|
|
let last_cmd = &self.last_cmd;
|
|
let layer_infos = &self.layer_infos;
|
|
let active_layer = &self.active_layer;
|
|
let active_color = self.active_color;
|
|
let active_linetype = &self.active_linetype;
|
|
let active_lineweight = self.active_lineweight;
|
|
let style_ctx = StyleContext {
|
|
text_style_names: self.text_style_names.clone(),
|
|
active_text_style: self.active_text_style.clone(),
|
|
dim_style_names: self.dim_style_names.clone(),
|
|
active_dim_style: self.active_dim_style.clone(),
|
|
mleader_style_names: self.mleader_style_names.clone(),
|
|
active_mleader_style: self.active_mleader_style.clone(),
|
|
table_style_names: self.table_style_names.clone(),
|
|
active_table_style: self.active_table_style.clone(),
|
|
};
|
|
|
|
let mut widgets: Vec<Element<Message>> = Vec::new();
|
|
let mut first_group = true;
|
|
|
|
for group in groups {
|
|
if !first_group {
|
|
widgets.push(
|
|
container(text(""))
|
|
.width(1)
|
|
.height(Fill)
|
|
.style(|_: &Theme| container::Style {
|
|
background: Some(Background::Color(BORDER_DARK)),
|
|
..Default::default()
|
|
})
|
|
.into(),
|
|
);
|
|
}
|
|
first_group = false;
|
|
|
|
let mut items_row: Vec<Element<Message>> = Vec::new();
|
|
let mut small_buf: Vec<Element<Message>> = Vec::new();
|
|
|
|
for item in group.tools {
|
|
let is_large = matches!(
|
|
&item,
|
|
RibbonItem::LargeTool(_)
|
|
| RibbonItem::LargeDropdown { .. }
|
|
| RibbonItem::LayerComboGroup { .. }
|
|
| RibbonItem::PropertiesGroup { .. }
|
|
| RibbonItem::StyleComboGroup { .. }
|
|
);
|
|
|
|
if is_large {
|
|
flush_small_col(&mut small_buf, &mut items_row);
|
|
items_row.push(render_large(
|
|
item,
|
|
&active_tool,
|
|
&open_dd,
|
|
last_cmd,
|
|
wireframe,
|
|
ortho_mode,
|
|
layer_infos,
|
|
active_layer,
|
|
active_color,
|
|
active_linetype,
|
|
active_lineweight,
|
|
&style_ctx,
|
|
));
|
|
} else {
|
|
small_buf.push(render_small(
|
|
item,
|
|
&active_tool,
|
|
&open_dd,
|
|
last_cmd,
|
|
wireframe,
|
|
ortho_mode,
|
|
));
|
|
if small_buf.len() == 3 {
|
|
flush_small_col(&mut small_buf, &mut items_row);
|
|
}
|
|
}
|
|
}
|
|
flush_small_col(&mut small_buf, &mut items_row);
|
|
|
|
let tools_el = items_row
|
|
.into_iter()
|
|
.fold(row![].spacing(2).height(Fill).align_y(iced::Top), |r, e| {
|
|
r.push(e)
|
|
});
|
|
|
|
widgets.push(
|
|
column![
|
|
tools_el,
|
|
container(text(group.title).size(9).color(GROUP_LABEL)).padding([1, 4]),
|
|
]
|
|
.spacing(0)
|
|
.padding([3u16, 4])
|
|
.height(Length::Fill)
|
|
.into(),
|
|
);
|
|
}
|
|
|
|
widgets
|
|
.into_iter()
|
|
.fold(row![].spacing(0).height(Length::Fill), |r, w| r.push(w))
|
|
.into()
|
|
} else {
|
|
text("").into()
|
|
};
|
|
|
|
let tool_bar = container(tool_area)
|
|
.style(|_: &Theme| container::Style {
|
|
background: Some(Background::Color(RIBBON_BG)),
|
|
border: Border {
|
|
color: BORDER_DARK,
|
|
width: 1.0,
|
|
radius: 0.0.into(),
|
|
},
|
|
..Default::default()
|
|
})
|
|
.width(Length::Fill)
|
|
.height(TOOL_BAR_H);
|
|
|
|
column![tab_bar, tool_bar].into()
|
|
}
|
|
|
|
// ── Dropdown overlay ──────────────────────────────────────────────────
|
|
|
|
pub fn dropdown_overlay(
|
|
&self,
|
|
undo_labels: &[String],
|
|
redo_labels: &[String],
|
|
) -> Option<Element<'_, Message>> {
|
|
let open_id = self.open_dropdown.as_deref()?;
|
|
|
|
if open_id == UNDO_HISTORY_ID || open_id == REDO_HISTORY_ID {
|
|
let is_undo = open_id == UNDO_HISTORY_ID;
|
|
let labels = if is_undo { undo_labels } else { redo_labels };
|
|
if labels.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
let rows: Vec<Element<Message>> = labels
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(idx, label)| {
|
|
let step = idx + 1;
|
|
button(text(label.clone()).size(11).color(LABEL_ON))
|
|
.on_press(if is_undo {
|
|
Message::UndoMany(step)
|
|
} else {
|
|
Message::RedoMany(step)
|
|
})
|
|
.style(|_: &Theme, status| button::Style {
|
|
background: Some(Background::Color(match status {
|
|
button::Status::Hovered | button::Status::Pressed => ROW_HOVER,
|
|
_ => Color::TRANSPARENT,
|
|
})),
|
|
..Default::default()
|
|
})
|
|
.width(Fill)
|
|
.padding([5, 10])
|
|
.into()
|
|
})
|
|
.collect();
|
|
|
|
let panel = container(column(rows))
|
|
.style(|_: &Theme| container::Style {
|
|
background: Some(Background::Color(PANEL_BG)),
|
|
border: Border {
|
|
color: PANEL_BORDER,
|
|
width: 1.0,
|
|
radius: 3.0.into(),
|
|
},
|
|
..Default::default()
|
|
})
|
|
.width(Length::Fixed(170.0));
|
|
|
|
let positioned = container(panel)
|
|
.align_left(Fill)
|
|
.align_top(Fill)
|
|
.padding(Padding {
|
|
top: 28.0,
|
|
left: compute_history_dropdown_left(open_id),
|
|
..Default::default()
|
|
})
|
|
.width(Fill)
|
|
.height(Fill);
|
|
|
|
return Some(
|
|
mouse_area(positioned)
|
|
.on_press(Message::CloseRibbonDropdown)
|
|
.into(),
|
|
);
|
|
}
|
|
|
|
if open_id == LAYER_COMBO_ID {
|
|
return self.layer_combo_overlay();
|
|
}
|
|
|
|
if open_id == PROP_COLOR_ID {
|
|
return self.prop_color_overlay();
|
|
}
|
|
if open_id == PROP_LINETYPE_ID {
|
|
return self.prop_linetype_overlay();
|
|
}
|
|
if open_id == PROP_LW_ID {
|
|
return self.prop_lw_overlay();
|
|
}
|
|
|
|
let module = self.modules.get(self.active)?;
|
|
let groups = module.ribbon_groups();
|
|
let mut items_list: Option<Vec<(&'static str, &'static str, IconKind)>> = None;
|
|
let mut dd_default = "";
|
|
let mut dd_id: &'static str = "";
|
|
|
|
'outer: for group in &groups {
|
|
for item in &group.tools {
|
|
let (id, items, default) = match item {
|
|
RibbonItem::Dropdown {
|
|
id, items, default, ..
|
|
} => (id, items, default),
|
|
RibbonItem::LargeDropdown {
|
|
id, items, default, ..
|
|
} => (id, items, default),
|
|
_ => continue,
|
|
};
|
|
if *id == open_id {
|
|
items_list = Some(items.clone());
|
|
dd_default = default;
|
|
dd_id = id;
|
|
break 'outer;
|
|
}
|
|
}
|
|
}
|
|
let items = items_list?;
|
|
let last_cmd = self.last_cmd.get(dd_id).copied().unwrap_or(dd_default);
|
|
|
|
let rows: Vec<Element<Message>> = items
|
|
.iter()
|
|
.map(|(cmd, label, item_icon)| {
|
|
let is_current = *cmd == last_cmd;
|
|
let checkmark = text(if is_current { "✓" } else { " " })
|
|
.size(11)
|
|
.color(if is_current {
|
|
CHECK_COLOR
|
|
} else {
|
|
Color::TRANSPARENT
|
|
})
|
|
.width(Length::Fixed(14.0));
|
|
let icon_el: Element<Message> = match *item_icon {
|
|
IconKind::Glyph(s) => text(s)
|
|
.size(13)
|
|
.color(ICON_COLOR)
|
|
.width(Length::Fixed(20.0))
|
|
.into(),
|
|
IconKind::Svg(bytes) => {
|
|
let handle = svg::Handle::from_memory(bytes);
|
|
svg(handle).width(20).height(20).into()
|
|
}
|
|
};
|
|
let label_el =
|
|
text(*label)
|
|
.size(11)
|
|
.color(if is_current { LABEL_ON } else { LABEL_OFF });
|
|
|
|
button(
|
|
row![checkmark, icon_el, label_el]
|
|
.spacing(4)
|
|
.align_y(iced::Center),
|
|
)
|
|
.on_press(Message::DropdownSelectItem {
|
|
dropdown_id: dd_id,
|
|
cmd: *cmd,
|
|
})
|
|
.style(|_: &Theme, status| button::Style {
|
|
background: Some(Background::Color(match status {
|
|
button::Status::Hovered | button::Status::Pressed => ROW_HOVER,
|
|
_ => Color::TRANSPARENT,
|
|
})),
|
|
..Default::default()
|
|
})
|
|
.width(Fill)
|
|
.padding([4, 10])
|
|
.into()
|
|
})
|
|
.collect();
|
|
|
|
let panel = container(column(rows))
|
|
.style(|_: &Theme| container::Style {
|
|
background: Some(Background::Color(PANEL_BG)),
|
|
border: Border {
|
|
color: PANEL_BORDER,
|
|
width: 1.0,
|
|
radius: 3.0.into(),
|
|
},
|
|
..Default::default()
|
|
})
|
|
.width(Length::Fixed(190.0));
|
|
|
|
let top_offset = 28.0 + TOOL_BAR_H;
|
|
let left_offset = compute_dropdown_left(&groups, open_id);
|
|
let positioned = container(panel)
|
|
.align_left(Fill)
|
|
.align_top(Fill)
|
|
.padding(Padding {
|
|
top: top_offset,
|
|
left: left_offset,
|
|
..Default::default()
|
|
})
|
|
.width(Fill)
|
|
.height(Fill);
|
|
|
|
Some(
|
|
mouse_area(positioned)
|
|
.on_press(Message::CloseRibbonDropdown)
|
|
.into(),
|
|
)
|
|
}
|
|
|
|
fn layer_combo_overlay(&self) -> Option<Element<'_, Message>> {
|
|
let rows: Vec<Element<Message>> = self
|
|
.layer_infos
|
|
.iter()
|
|
.map(|info| {
|
|
let is_active = info.name == self.active_layer;
|
|
let lc = info.color;
|
|
let lv = info.visible;
|
|
let lf = info.frozen;
|
|
let ll = info.locked;
|
|
let name = info.name.clone();
|
|
|
|
let swatch = container(text(""))
|
|
.style(move |_: &Theme| container::Style {
|
|
background: Some(Background::Color(lc)),
|
|
border: Border {
|
|
color: Color {
|
|
r: 0.0,
|
|
g: 0.0,
|
|
b: 0.0,
|
|
a: 0.5,
|
|
},
|
|
width: 1.0,
|
|
radius: 1.0.into(),
|
|
},
|
|
..Default::default()
|
|
})
|
|
.width(12)
|
|
.height(12);
|
|
|
|
let vis = text(if lv { "●" } else { "○" }).size(10).color(if lv {
|
|
Color {
|
|
r: 0.95,
|
|
g: 0.85,
|
|
b: 0.20,
|
|
a: 1.0,
|
|
}
|
|
} else {
|
|
Color {
|
|
r: 0.45,
|
|
g: 0.45,
|
|
b: 0.45,
|
|
a: 1.0,
|
|
}
|
|
});
|
|
let freeze = text("✱").size(10).color(if lf {
|
|
Color {
|
|
r: 0.40,
|
|
g: 0.80,
|
|
b: 1.00,
|
|
a: 1.0,
|
|
}
|
|
} else {
|
|
Color {
|
|
r: 0.95,
|
|
g: 0.85,
|
|
b: 0.20,
|
|
a: 1.0,
|
|
}
|
|
});
|
|
let lock = text(if ll { "🔒" } else { "🔓" }).size(10).color(if ll {
|
|
Color {
|
|
r: 0.95,
|
|
g: 0.70,
|
|
b: 0.20,
|
|
a: 1.0,
|
|
}
|
|
} else {
|
|
Color {
|
|
r: 0.55,
|
|
g: 0.55,
|
|
b: 0.55,
|
|
a: 1.0,
|
|
}
|
|
});
|
|
let checkmark = text(if is_active { "✓" } else { " " })
|
|
.size(11)
|
|
.color(if is_active {
|
|
CHECK_COLOR
|
|
} else {
|
|
Color::TRANSPARENT
|
|
})
|
|
.width(Length::Fixed(14.0));
|
|
let label =
|
|
text(&info.name)
|
|
.size(11)
|
|
.color(if is_active { LABEL_ON } else { LABEL_OFF });
|
|
|
|
button(
|
|
row![checkmark, vis, freeze, lock, swatch, label]
|
|
.spacing(5)
|
|
.align_y(iced::Center),
|
|
)
|
|
.on_press(Message::RibbonLayerChanged(name))
|
|
.style(|_: &Theme, status| button::Style {
|
|
background: Some(Background::Color(match status {
|
|
button::Status::Hovered | button::Status::Pressed => ROW_HOVER,
|
|
_ => Color::TRANSPARENT,
|
|
})),
|
|
..Default::default()
|
|
})
|
|
.width(Fill)
|
|
.padding([4, 8])
|
|
.into()
|
|
})
|
|
.collect();
|
|
|
|
let panel = container(column(rows))
|
|
.style(|_: &Theme| container::Style {
|
|
background: Some(Background::Color(PANEL_BG)),
|
|
border: Border {
|
|
color: PANEL_BORDER,
|
|
width: 1.0,
|
|
radius: 3.0.into(),
|
|
},
|
|
..Default::default()
|
|
})
|
|
.width(Length::Fixed(220.0));
|
|
|
|
let top_offset = 28.0 + TOOL_BAR_H;
|
|
let groups = self.modules.get(self.active)?.ribbon_groups();
|
|
let left_offset = compute_layer_combo_left(&groups);
|
|
let positioned = container(panel)
|
|
.align_left(Fill)
|
|
.align_top(Fill)
|
|
.padding(Padding {
|
|
top: top_offset,
|
|
left: left_offset,
|
|
..Default::default()
|
|
})
|
|
.width(Fill)
|
|
.height(Fill);
|
|
|
|
Some(
|
|
mouse_area(positioned)
|
|
.on_press(Message::CloseRibbonDropdown)
|
|
.into(),
|
|
)
|
|
}
|
|
|
|
fn prop_color_overlay(&self) -> Option<Element<'_, Message>> {
|
|
let picker = color_picker_dropdown(
|
|
self.prop_color_palette_open,
|
|
Message::RibbonColorPaletteToggle,
|
|
Some(Message::RibbonColorChanged(AcadColor::ByLayer)),
|
|
Some(Message::RibbonColorChanged(AcadColor::ByBlock)),
|
|
|aci| Message::RibbonColorChanged(AcadColor::Index(aci)),
|
|
);
|
|
|
|
let panel = container(picker)
|
|
.style(|_: &Theme| container::Style {
|
|
background: Some(Background::Color(PANEL_BG)),
|
|
border: Border {
|
|
color: PANEL_BORDER,
|
|
width: 1.0,
|
|
radius: 3.0.into(),
|
|
},
|
|
..Default::default()
|
|
})
|
|
.width(Length::Fixed(200.0));
|
|
|
|
let top_offset = 28.0 + TOOL_BAR_H;
|
|
let groups = self.modules.get(self.active)?.ribbon_groups();
|
|
let left_offset = compute_prop_combo_left(&groups, PROP_COLOR_ID);
|
|
let positioned = container(panel)
|
|
.align_left(Fill)
|
|
.align_top(Fill)
|
|
.padding(Padding {
|
|
top: top_offset,
|
|
left: left_offset,
|
|
..Default::default()
|
|
})
|
|
.width(Fill)
|
|
.height(Fill);
|
|
|
|
Some(
|
|
mouse_area(positioned)
|
|
.on_press(Message::CloseRibbonDropdown)
|
|
.into(),
|
|
)
|
|
}
|
|
|
|
fn prop_linetype_overlay(&self) -> Option<Element<'_, Message>> {
|
|
let active_lt = &self.active_linetype;
|
|
|
|
let mut items: Vec<LinetypeItem> = vec![
|
|
LinetypeItem {
|
|
name: "ByLayer".to_string(),
|
|
art: String::new(),
|
|
},
|
|
LinetypeItem {
|
|
name: "ByBlock".to_string(),
|
|
art: String::new(),
|
|
},
|
|
];
|
|
for lt in &self.available_linetypes {
|
|
if lt.name != "ByLayer" && lt.name != "ByBlock" {
|
|
items.push(lt.clone());
|
|
}
|
|
}
|
|
|
|
let rows: Vec<Element<Message>> = items
|
|
.into_iter()
|
|
.map(|lt| {
|
|
let is_cur = lt.name == *active_lt;
|
|
let check = text(if is_cur { "✓" } else { " " })
|
|
.size(11)
|
|
.color(if is_cur {
|
|
CHECK_COLOR
|
|
} else {
|
|
Color::TRANSPARENT
|
|
})
|
|
.width(Length::Fixed(14.0));
|
|
let name_col = text(lt.name.clone())
|
|
.size(11)
|
|
.color(if is_cur { LABEL_ON } else { LABEL_OFF })
|
|
.width(Length::Fixed(90.0));
|
|
let art_col = text(lt.art.clone()).size(9).color(Color {
|
|
r: 0.55,
|
|
g: 0.55,
|
|
b: 0.55,
|
|
a: 1.0,
|
|
});
|
|
let name = lt.name.clone();
|
|
button(
|
|
row![check, name_col, art_col]
|
|
.spacing(4)
|
|
.align_y(iced::Center),
|
|
)
|
|
.on_press(Message::RibbonLinetypeChanged(name))
|
|
.style(|_: &Theme, status| button::Style {
|
|
background: Some(Background::Color(match status {
|
|
button::Status::Hovered | button::Status::Pressed => ROW_HOVER,
|
|
_ => Color::TRANSPARENT,
|
|
})),
|
|
..Default::default()
|
|
})
|
|
.width(Fill)
|
|
.padding([4, 6])
|
|
.into()
|
|
})
|
|
.collect();
|
|
|
|
let list = container(scrollable(column(rows)).height(Length::Fixed(200.0)))
|
|
.style(|_: &Theme| container::Style {
|
|
background: Some(Background::Color(PANEL_BG)),
|
|
border: Border {
|
|
color: PANEL_BORDER,
|
|
width: 1.0,
|
|
radius: 3.0.into(),
|
|
},
|
|
..Default::default()
|
|
})
|
|
.width(Length::Fixed(220.0));
|
|
|
|
let top_offset = 28.0 + TOOL_BAR_H;
|
|
let groups = self.modules.get(self.active)?.ribbon_groups();
|
|
let left_offset = compute_prop_combo_left(&groups, PROP_LINETYPE_ID);
|
|
let positioned = container(list)
|
|
.align_left(Fill)
|
|
.align_top(Fill)
|
|
.padding(Padding {
|
|
top: top_offset,
|
|
left: left_offset,
|
|
..Default::default()
|
|
})
|
|
.width(Fill)
|
|
.height(Fill);
|
|
|
|
Some(
|
|
mouse_area(positioned)
|
|
.on_press(Message::CloseRibbonDropdown)
|
|
.into(),
|
|
)
|
|
}
|
|
|
|
fn prop_lw_overlay(&self) -> Option<Element<'_, Message>> {
|
|
let active_lw = self.active_lineweight;
|
|
let rows: Vec<Element<Message>> = lw_options()
|
|
.into_iter()
|
|
.map(|item| {
|
|
let is_cur = item.0 == active_lw;
|
|
let label = item.to_string();
|
|
let check = text(if is_cur { "✓" } else { " " })
|
|
.size(11)
|
|
.color(if is_cur {
|
|
CHECK_COLOR
|
|
} else {
|
|
Color::TRANSPARENT
|
|
})
|
|
.width(Length::Fixed(14.0));
|
|
button(
|
|
row![
|
|
check,
|
|
text(label)
|
|
.size(11)
|
|
.color(if is_cur { LABEL_ON } else { LABEL_OFF })
|
|
]
|
|
.spacing(5)
|
|
.align_y(iced::Center),
|
|
)
|
|
.on_press(Message::RibbonLineweightChanged(item.0))
|
|
.style(|_: &Theme, status| button::Style {
|
|
background: Some(Background::Color(match status {
|
|
button::Status::Hovered | button::Status::Pressed => ROW_HOVER,
|
|
_ => Color::TRANSPARENT,
|
|
})),
|
|
..Default::default()
|
|
})
|
|
.width(Fill)
|
|
.padding([4, 8])
|
|
.into()
|
|
})
|
|
.collect();
|
|
|
|
self.prop_overlay_positioned(rows, PROP_LW_ID, 140.0)
|
|
}
|
|
|
|
fn prop_overlay_positioned<'a>(
|
|
&'a self,
|
|
rows: Vec<Element<'a, Message>>,
|
|
dd_id: &str,
|
|
width: f32,
|
|
) -> Option<Element<'a, Message>> {
|
|
let panel = container(column(rows))
|
|
.style(|_: &Theme| container::Style {
|
|
background: Some(Background::Color(PANEL_BG)),
|
|
border: Border {
|
|
color: PANEL_BORDER,
|
|
width: 1.0,
|
|
radius: 3.0.into(),
|
|
},
|
|
..Default::default()
|
|
})
|
|
.width(Length::Fixed(width));
|
|
|
|
let top_offset = 28.0 + TOOL_BAR_H;
|
|
let groups = self.modules.get(self.active)?.ribbon_groups();
|
|
let left_offset = compute_prop_combo_left(&groups, dd_id);
|
|
let positioned = container(panel)
|
|
.align_left(Fill)
|
|
.align_top(Fill)
|
|
.padding(Padding {
|
|
top: top_offset,
|
|
left: left_offset,
|
|
..Default::default()
|
|
})
|
|
.width(Fill)
|
|
.height(Fill);
|
|
|
|
Some(
|
|
mouse_area(positioned)
|
|
.on_press(Message::CloseRibbonDropdown)
|
|
.into(),
|
|
)
|
|
}
|
|
}
|
|
|
|
impl Default for Ribbon {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|