forked from 1b-agent/opencadstudio-upstream
Merge main into Select-Color-Redesign
Resolve conflicts with the new gradient and graphic-attributes colour targets: keep main's targets and menu handling, routed through the new ColorPicker state; adapt dock/view.rs to `color_picker`. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013rGmDUNr1hUf4zo1X3dak8
This commit is contained in:
220 files changed
+37313
-5826
No files matched your search
+15
-13
@@ -203,18 +203,7 @@ fn color_selector_with_indicator<'a>(
|
||||
}
|
||||
|
||||
let popup = container(color_list(extras, on_select, on_more))
|
||||
.style(|theme: &Theme| {
|
||||
let palette = theme.palette();
|
||||
container::Style {
|
||||
background: Some(Background::Color(palette.background.weak.color)),
|
||||
border: Border {
|
||||
color: palette.background.neutral.color,
|
||||
width: 1.0,
|
||||
radius: 2.0.into(),
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
})
|
||||
.style(popup_panel_style)
|
||||
.padding(2);
|
||||
|
||||
// `DropDown` keeps the popup outside the surrounding form layout and
|
||||
@@ -228,7 +217,20 @@ fn color_selector_with_indicator<'a>(
|
||||
.into()
|
||||
}
|
||||
|
||||
fn list_row_style(theme: &Theme, status: button::Status) -> button::Style {
|
||||
pub(crate) fn popup_panel_style(theme: &Theme) -> container::Style {
|
||||
let palette = theme.palette();
|
||||
container::Style {
|
||||
background: Some(Background::Color(palette.background.weak.color)),
|
||||
border: Border {
|
||||
color: palette.background.neutral.color,
|
||||
width: 1.0,
|
||||
radius: 2.0.into(),
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn list_row_style(theme: &Theme, status: button::Status) -> button::Style {
|
||||
let palette = theme.palette();
|
||||
let hovered = matches!(status, button::Status::Hovered);
|
||||
let text_color = if hovered {
|
||||
|
||||
+1254
-181
File diff suppressed because it is too large.
Load diff
+9
-2
@@ -73,7 +73,11 @@ pub static COPY: &[u8] = include_bytes!("../../assets/icons/ui/copy.svg");
|
||||
pub static MENU: &[u8] = include_bytes!("../../assets/icons/ui/menu.svg");
|
||||
pub static MOVE: &[u8] = include_bytes!("../../assets/icons/ui/move.svg");
|
||||
pub static RESIZE: &[u8] = include_bytes!("../../assets/icons/ui/resize.svg");
|
||||
/// `RESIZE` mirrored for a bottom-left corner grip.
|
||||
pub static RESIZE_LEFT: &[u8] = include_bytes!("../../assets/icons/ui/resize_left.svg");
|
||||
pub static PIN: &[u8] = include_bytes!("../../assets/icons/ui/pin.svg");
|
||||
/// `PIN` tilted 30°: the pin while auto-hide is on.
|
||||
pub static PIN_ACTIVE: &[u8] = include_bytes!("../../assets/icons/ui/pin_active.svg");
|
||||
pub static SPLIT_V: &[u8] = include_bytes!("../../assets/icons/ui/split_v.svg");
|
||||
pub static SPLIT_H: &[u8] = include_bytes!("../../assets/icons/ui/split_h.svg");
|
||||
pub static GRID: &[u8] = include_bytes!("../../assets/icons/ui/grid.svg");
|
||||
@@ -510,13 +514,16 @@ pub fn themed_secondary<'a, M: 'a>(bytes: &'static [u8], size: f32) -> Element<'
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Render disabled chrome with the active Iced theme's text color.
|
||||
/// Render disabled chrome with the active Iced theme's text color, faded
|
||||
/// like disabled text. The fade goes through the widget's opacity: an SVG
|
||||
/// tint only replaces RGB, so alpha on the style colour would be ignored.
|
||||
pub fn themed_disabled<'a, M: 'a>(bytes: &'static [u8], size: f32) -> Element<'a, M> {
|
||||
svg(themed_handle(bytes))
|
||||
.width(size)
|
||||
.height(size)
|
||||
.opacity(0.42_f32)
|
||||
.style(|theme: &Theme, _| svg::Style {
|
||||
color: Some(theme.palette().background.base.text.scale_alpha(0.42)),
|
||||
color: Some(theme.palette().background.base.text),
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
+3
-11
@@ -658,7 +658,7 @@ impl Graph {
|
||||
/// The node library as a docked panel: title bar, a search box with the
|
||||
/// graph's New / Open / Save beside it, then the categories. Entries are
|
||||
/// clicked or dragged onto the canvas.
|
||||
pub fn panel(&self, width: f32, auto_collapse: bool) -> Element<'_, Message> {
|
||||
pub fn panel(&self, width: f32, chrome: crate::ui::dock::Chrome) -> Element<'_, Message> {
|
||||
use crate::ui::dock::{self, PanelId};
|
||||
let tool = |icon: &'static [u8], tip: &str, message: GraphMsg| {
|
||||
dock::tool_button(
|
||||
@@ -740,16 +740,8 @@ impl Graph {
|
||||
.height(Length::Fill);
|
||||
|
||||
dock::frame(
|
||||
column![
|
||||
dock::title_bar(
|
||||
PanelId::NodeGraph,
|
||||
t!("Node Graph").into_owned(),
|
||||
auto_collapse
|
||||
),
|
||||
toolbar,
|
||||
body
|
||||
]
|
||||
.spacing(6),
|
||||
dock::title_bar(PanelId::NodeGraph, t!("Node Graph").into_owned(), chrome),
|
||||
column![toolbar, body].spacing(6),
|
||||
width,
|
||||
)
|
||||
}
|
||||
|
||||
+154
-96
@@ -55,6 +55,9 @@ pub fn linetype_display_name(name: &str) -> String {
|
||||
|
||||
impl fmt::Display for LinetypeItem {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if self.name.is_empty() && !self.art.is_empty() {
|
||||
return f.write_str(&self.art);
|
||||
}
|
||||
let name = linetype_display_name(&self.name);
|
||||
if self.art.is_empty() {
|
||||
write!(f, "{name}")
|
||||
@@ -95,6 +98,7 @@ impl fmt::Display for SelectionGroup {
|
||||
#[derive(Clone)]
|
||||
struct HatchPatternPreview {
|
||||
pattern: crate::scene::model::hatch_model::HatchPattern,
|
||||
padding: f32,
|
||||
}
|
||||
|
||||
impl canvas::Program<Message> for HatchPatternPreview {
|
||||
@@ -112,7 +116,7 @@ impl canvas::Program<Message> for HatchPatternPreview {
|
||||
|
||||
let mut frame = canvas::Frame::new(renderer, bounds.size());
|
||||
let palette = theme.palette();
|
||||
let pad = 4.0;
|
||||
let pad = self.padding;
|
||||
let sample = canvas::Path::rectangle(
|
||||
Point::new(pad, pad),
|
||||
Size::new(
|
||||
@@ -201,6 +205,31 @@ fn hatch_preview_scale(pattern: &crate::scene::model::hatch_model::HatchPattern)
|
||||
}
|
||||
}
|
||||
|
||||
fn hatch_pattern_preview(
|
||||
pattern: crate::scene::model::hatch_model::HatchPattern,
|
||||
height: f32,
|
||||
) -> Element<'static, Message> {
|
||||
canvas(HatchPatternPreview {
|
||||
pattern,
|
||||
padding: 4.0,
|
||||
})
|
||||
.width(Length::Fill)
|
||||
.height(height)
|
||||
.into()
|
||||
}
|
||||
|
||||
pub(crate) fn compact_hatch_pattern_preview(
|
||||
pattern: crate::scene::model::hatch_model::HatchPattern,
|
||||
) -> Element<'static, Message> {
|
||||
canvas(HatchPatternPreview {
|
||||
pattern,
|
||||
padding: 0.5,
|
||||
})
|
||||
.width(Length::Fill)
|
||||
.height(ROW_H)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn hatch_pattern_matches(
|
||||
entry: &crate::scene::model::hatch_patterns::PatternEntry,
|
||||
search: &str,
|
||||
@@ -223,6 +252,113 @@ pub(crate) fn filtered_hatch_patterns(
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Messages of a hatch pattern picker; the Properties panel and the Graphic
|
||||
/// Attributes palette each route them to their own state.
|
||||
pub(crate) struct PatternPickerMessages {
|
||||
pub search_id: &'static str,
|
||||
pub on_search: fn(String) -> Message,
|
||||
pub on_confirm: Message,
|
||||
pub on_focus: fn(usize) -> Message,
|
||||
pub on_changed: fn(String) -> Message,
|
||||
}
|
||||
|
||||
pub(crate) fn hatch_pattern_picker_content<'a>(
|
||||
search_value: &'a str,
|
||||
focus: usize,
|
||||
current: &'a str,
|
||||
messages: PatternPickerMessages,
|
||||
) -> Element<'a, Message> {
|
||||
let PatternPickerMessages {
|
||||
search_id,
|
||||
on_search,
|
||||
on_confirm,
|
||||
on_focus,
|
||||
on_changed,
|
||||
} = messages;
|
||||
let search = text_input(t!("Search patterns…").as_ref(), search_value)
|
||||
.id(iced::widget::Id::new(search_id))
|
||||
.on_input(on_search)
|
||||
.on_submit(on_confirm)
|
||||
.size(FONT_SZ)
|
||||
.padding([5, 7])
|
||||
.width(Length::Fill);
|
||||
|
||||
let mut grid = column![].spacing(6);
|
||||
let visible = filtered_hatch_patterns(search_value);
|
||||
for (row_index, pair) in visible.chunks(2).enumerate() {
|
||||
let mut cards = row![].spacing(6);
|
||||
for (column_index, entry) in pair.iter().enumerate() {
|
||||
let index = row_index * 2 + column_index;
|
||||
let selected = current.eq_ignore_ascii_case(&entry.name);
|
||||
let focused = focus == index;
|
||||
let name = entry.name.clone();
|
||||
let preview = hatch_pattern_preview(entry.gpu.clone(), PATTERN_PREVIEW_H);
|
||||
let card = button(
|
||||
column![
|
||||
preview,
|
||||
container(text(crate::ui::text_util::elide(&entry.name, 20)).size(FONT_SZ))
|
||||
.width(Length::Fill)
|
||||
.align_x(iced::Center),
|
||||
]
|
||||
.spacing(3),
|
||||
)
|
||||
.on_press(on_changed(name))
|
||||
.style(move |theme: &Theme, status| {
|
||||
let palette = theme.palette();
|
||||
let hovered = matches!(status, button::Status::Hovered | button::Status::Pressed);
|
||||
let pair = if selected {
|
||||
palette.primary.weak
|
||||
} else if hovered || focused {
|
||||
palette.background.strong
|
||||
} else {
|
||||
palette.background.weak
|
||||
};
|
||||
button::Style {
|
||||
background: Some(Background::Color(pair.color)),
|
||||
text_color: pair.text,
|
||||
border: Border {
|
||||
color: if selected || focused {
|
||||
palette.primary.base.color
|
||||
} else {
|
||||
palette.background.neutral.color
|
||||
},
|
||||
width: if selected || focused { 2.0 } else { 1.0 },
|
||||
radius: 4.0.into(),
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
})
|
||||
.padding(5)
|
||||
.width(PATTERN_CARD_W);
|
||||
cards = cards.push(mouse_area(card).on_enter(on_focus(index)));
|
||||
}
|
||||
grid = grid.push(cards);
|
||||
}
|
||||
|
||||
let results: Element<'_, Message> = if visible.is_empty() {
|
||||
container(
|
||||
text(t!("No matching patterns"))
|
||||
.size(FONT_SZ)
|
||||
.style(hint_text_style),
|
||||
)
|
||||
.padding(12)
|
||||
.width(Length::Fill)
|
||||
.center_x(Length::Fill)
|
||||
.into()
|
||||
} else {
|
||||
scrollable(grid)
|
||||
.height(Length::Fill)
|
||||
.width(Length::Fill)
|
||||
.into()
|
||||
};
|
||||
container(column![search, results].spacing(7))
|
||||
.style(container::bordered_box)
|
||||
.padding(8)
|
||||
.width(PATTERN_PICKER_W)
|
||||
.height(Length::Fixed(PATTERN_PICKER_H))
|
||||
.into()
|
||||
}
|
||||
|
||||
/// All standard CAD lineweight options for the combobox.
|
||||
pub fn lw_options() -> Vec<LwItem> {
|
||||
[
|
||||
@@ -544,14 +680,11 @@ impl PropertiesPanel {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn view(&self, width: f32, auto_collapse: bool) -> Element<'_, Message> {
|
||||
pub fn view(&self, width: f32, chrome: crate::ui::dock::Chrome) -> Element<'_, Message> {
|
||||
use crate::ui::dock::PanelId;
|
||||
// ── Header ──────────────────────────────────────────────────────────
|
||||
let header = crate::ui::dock::title_bar(
|
||||
PanelId::Properties,
|
||||
t!("Properties").into_owned(),
|
||||
auto_collapse,
|
||||
);
|
||||
let header =
|
||||
crate::ui::dock::title_bar(PanelId::Properties, t!("Properties").into_owned(), chrome);
|
||||
|
||||
// ── Title bar (entity type / "No Selection") ─────────────────────
|
||||
let title_content: Element<'_, Message> = if self.selection_groups.is_empty() {
|
||||
@@ -1507,99 +1640,24 @@ impl PropertiesPanel {
|
||||
return prop_row_widget(label, head.into());
|
||||
}
|
||||
|
||||
let search = text_input(t!("Search patterns…").as_ref(), &self.hatch_pattern_search)
|
||||
.id(iced::widget::Id::new("hatch-pattern-search"))
|
||||
.on_input(Message::PropHatchPatternSearchChanged)
|
||||
.on_submit(Message::PropHatchPatternConfirm)
|
||||
.size(FONT_SZ)
|
||||
.padding([5, 7])
|
||||
.width(Length::Fill);
|
||||
|
||||
let mut grid = column![].spacing(6);
|
||||
let visible = filtered_hatch_patterns(&self.hatch_pattern_search);
|
||||
for (row_index, pair) in visible.chunks(2).enumerate() {
|
||||
let mut cards = row![].spacing(6);
|
||||
for (column_index, entry) in pair.iter().enumerate() {
|
||||
let index = row_index * 2 + column_index;
|
||||
let selected = current.eq_ignore_ascii_case(&entry.name);
|
||||
let focused = self.hatch_pattern_focus == index;
|
||||
let name = entry.name.clone();
|
||||
let preview = canvas(HatchPatternPreview {
|
||||
pattern: entry.gpu.clone(),
|
||||
})
|
||||
.width(Length::Fill)
|
||||
.height(PATTERN_PREVIEW_H);
|
||||
let card = button(
|
||||
column![
|
||||
preview,
|
||||
container(text(crate::ui::text_util::elide(&entry.name, 20)).size(FONT_SZ))
|
||||
.width(Length::Fill)
|
||||
.align_x(iced::Center),
|
||||
]
|
||||
.spacing(3),
|
||||
)
|
||||
.on_press(Message::PropHatchPatternChanged(name))
|
||||
.style(move |theme: &Theme, status| {
|
||||
let palette = theme.palette();
|
||||
let hovered =
|
||||
matches!(status, button::Status::Hovered | button::Status::Pressed);
|
||||
let pair = if selected {
|
||||
palette.primary.weak
|
||||
} else if hovered || focused {
|
||||
palette.background.strong
|
||||
} else {
|
||||
palette.background.weak
|
||||
};
|
||||
button::Style {
|
||||
background: Some(Background::Color(pair.color)),
|
||||
text_color: pair.text,
|
||||
border: Border {
|
||||
color: if selected || focused {
|
||||
palette.primary.base.color
|
||||
} else {
|
||||
palette.background.neutral.color
|
||||
},
|
||||
width: if selected || focused { 2.0 } else { 1.0 },
|
||||
radius: 4.0.into(),
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
})
|
||||
.padding(5)
|
||||
.width(PATTERN_CARD_W);
|
||||
cards =
|
||||
cards.push(mouse_area(card).on_enter(Message::PropHatchPatternFocus(index)));
|
||||
}
|
||||
grid = grid.push(cards);
|
||||
}
|
||||
|
||||
let results: Element<'_, Message> = if visible.is_empty() {
|
||||
container(
|
||||
text(t!("No matching patterns"))
|
||||
.size(FONT_SZ)
|
||||
.style(hint_text_style),
|
||||
)
|
||||
.padding(12)
|
||||
.width(Length::Fill)
|
||||
.center_x(Length::Fill)
|
||||
.into()
|
||||
} else {
|
||||
scrollable(grid)
|
||||
.height(Length::Fill)
|
||||
.width(Length::Fill)
|
||||
.into()
|
||||
};
|
||||
let popup = container(column![search, results].spacing(7))
|
||||
.style(container::bordered_box)
|
||||
.padding(8)
|
||||
.width(PATTERN_PICKER_W)
|
||||
.height(Length::Fixed(PATTERN_PICKER_H));
|
||||
let popup = hatch_pattern_picker_content(
|
||||
&self.hatch_pattern_search,
|
||||
self.hatch_pattern_focus,
|
||||
current,
|
||||
PatternPickerMessages {
|
||||
search_id: "hatch-pattern-search",
|
||||
on_search: Message::PropHatchPatternSearchChanged,
|
||||
on_confirm: Message::PropHatchPatternConfirm,
|
||||
on_focus: Message::PropHatchPatternFocus,
|
||||
on_changed: Message::PropHatchPatternChanged,
|
||||
},
|
||||
);
|
||||
|
||||
prop_row_widget(
|
||||
label,
|
||||
crate::ui::color_select::drop_down_below(
|
||||
head.into(),
|
||||
popup.into(),
|
||||
popup,
|
||||
Some(Length::Fixed(PATTERN_PICKER_W)),
|
||||
Length::Fixed(PATTERN_PICKER_H),
|
||||
Message::PropHatchPatternPickerToggle(current.to_string()),
|
||||
@@ -2161,7 +2219,7 @@ fn text_input_style(theme: &Theme, status: text_input::Status) -> text_input::St
|
||||
}
|
||||
}
|
||||
|
||||
fn combo_input_style(theme: &Theme, status: text_input::Status) -> text_input::Style {
|
||||
pub(crate) fn combo_input_style(theme: &Theme, status: text_input::Status) -> text_input::Style {
|
||||
text_input_style(theme, status)
|
||||
}
|
||||
|
||||
|
||||
@@ -62,6 +62,8 @@ pub struct Ribbon {
|
||||
pub show_count_palette: bool,
|
||||
/// Sheet Set Manager palette shown (SSMSTATE).
|
||||
pub show_sheet_set: bool,
|
||||
/// Graphic Attributes panel visibility — drives its ribbon button highlight.
|
||||
pub show_graphic_attributes: bool,
|
||||
/// File tabs (FILETAB) visibility — drives the File Tabs button highlight.
|
||||
pub show_file_tabs: bool,
|
||||
/// Layout tabs (LAYOUTTAB) visibility — drives the Layout Tabs button highlight.
|
||||
@@ -181,6 +183,7 @@ impl Ribbon {
|
||||
show_properties: true,
|
||||
show_count_palette: false,
|
||||
show_sheet_set: false,
|
||||
show_graphic_attributes: false,
|
||||
show_file_tabs: true,
|
||||
show_layout_tabs: true,
|
||||
open_dropdown: None,
|
||||
@@ -346,6 +349,9 @@ impl Ribbon {
|
||||
pub fn set_sheet_set(&mut self, on: bool) {
|
||||
self.show_sheet_set = on;
|
||||
}
|
||||
pub fn set_graphic_attributes(&mut self, on: bool) {
|
||||
self.show_graphic_attributes = on;
|
||||
}
|
||||
pub fn set_file_tabs(&mut self, on: bool) {
|
||||
self.show_file_tabs = on;
|
||||
}
|
||||
@@ -364,6 +370,7 @@ impl Ribbon {
|
||||
show_ucs_icon: self.show_ucs_icon,
|
||||
show_properties: self.show_properties,
|
||||
show_count_palette: self.show_count_palette,
|
||||
show_graphic_attributes: self.show_graphic_attributes,
|
||||
show_block_palette,
|
||||
show_sheet_set: self.show_sheet_set,
|
||||
show_file_tabs: self.show_file_tabs,
|
||||
|
||||
@@ -38,6 +38,7 @@ pub(super) struct ToggleState {
|
||||
pub show_ucs_icon: bool,
|
||||
pub show_properties: bool,
|
||||
pub show_count_palette: bool,
|
||||
pub show_graphic_attributes: bool,
|
||||
pub show_block_palette: bool,
|
||||
pub show_sheet_set: bool,
|
||||
pub show_file_tabs: bool,
|
||||
@@ -370,6 +371,7 @@ pub(super) fn is_active_tool(id: &str, active_tool: &Option<String>, state: &Tog
|
||||
"UCSICON" => state.show_ucs_icon,
|
||||
"PROPERTIES" => state.show_properties,
|
||||
"COUNTLIST" => state.show_count_palette,
|
||||
"GRAPHICATTRIBUTES" => state.show_graphic_attributes,
|
||||
"BLOCKPALETTE" => state.show_block_palette,
|
||||
"SHEETSET" => state.show_sheet_set,
|
||||
"FILETAB" => state.show_file_tabs,
|
||||
|
||||
@@ -329,7 +329,9 @@ pub fn view_window<'a>(
|
||||
.iter()
|
||||
.map(|value| (*value).to_string())
|
||||
.collect();
|
||||
let system = crate::scene::text::sysfont::families().to_vec();
|
||||
// Web: the fetched Noto set plus the office/CAD family names it
|
||||
// stands in for (Arial → Noto Sans, …); desktop: installed fonts.
|
||||
let system = crate::scene::text::sysfont::picker_families();
|
||||
column![
|
||||
row![
|
||||
font_list(t!("Stroke fonts"), built_in, v.font_buf, false, v.read_only),
|
||||
|
||||
@@ -899,6 +899,17 @@ pub fn data_extraction_view(
|
||||
let add_folder = button(text(t!("Add folder…")).size(12)).padding([5, 10]);
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let add_folder = add_folder.on_press(Message::DataExtractionAddFolder);
|
||||
// The web has no folder picker and no directory to walk, so the button
|
||||
// stays disabled there — with a tooltip that says why and what to do.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let add_folder = iced::widget::tooltip(
|
||||
add_folder,
|
||||
container(text(t!("Adding folders is not available in the browser. Add drawing files instead.")).size(11))
|
||||
.padding([3, 6])
|
||||
.style(container::bordered_box),
|
||||
iced::widget::tooltip::Position::Top,
|
||||
)
|
||||
.gap(4);
|
||||
|
||||
let body: Element<'_, Message> = match state.page {
|
||||
ExtractionPage::Begin => group(
|
||||
|
||||
@@ -139,6 +139,15 @@ impl BlockDefinitionState {
|
||||
glam::DVec3::new(x, y, z)
|
||||
}
|
||||
|
||||
/// Prefill the base point fields from an existing definition, so
|
||||
/// redefining a block starts from the base it already has (0.0000 for a
|
||||
/// new block). Formatted the same way the pick point button writes them.
|
||||
pub fn set_base_point(&mut self, base: glam::DVec3) {
|
||||
self.base_point_x = format!("{:.4}", base.x);
|
||||
self.base_point_y = format!("{:.4}", base.y);
|
||||
self.base_point_z = format!("{:.4}", base.z);
|
||||
}
|
||||
|
||||
pub fn is_existing_name(&self) -> bool {
|
||||
let trimmed = self.name.trim();
|
||||
!trimmed.is_empty()
|
||||
|
||||
@@ -167,6 +167,10 @@ pub enum BlockPaletteMsg {
|
||||
View(ViewMode),
|
||||
PickFile,
|
||||
FilePicked(Result<PathBuf, String>),
|
||||
/// Web: the picked drawing arrived already parsed from the picker's
|
||||
/// bytes — a name-only path has nothing behind it to read later.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
FilePickedDoc(Result<(String, codec::CadDocument), String>),
|
||||
Refresh,
|
||||
Option(OptionMsg),
|
||||
ToggleOptions,
|
||||
@@ -800,11 +804,15 @@ fn options_card<'a>(palette: &'a BlockPalette) -> Element<'a, Message> {
|
||||
}
|
||||
|
||||
/// Build the docked panel element from the palette state.
|
||||
pub fn view(palette: &BlockPalette, width: f32, auto_collapse: bool) -> Element<'_, Message> {
|
||||
pub fn view(
|
||||
palette: &BlockPalette,
|
||||
width: f32,
|
||||
chrome: crate::ui::dock::Chrome,
|
||||
) -> Element<'_, Message> {
|
||||
let title_bar = crate::ui::dock::title_bar(
|
||||
PanelId::BlockPalette,
|
||||
crate::t!("Blocks").into_owned(),
|
||||
auto_collapse,
|
||||
chrome,
|
||||
);
|
||||
|
||||
let tabs = {
|
||||
@@ -991,7 +999,8 @@ pub fn view(palette: &BlockPalette, width: f32, auto_collapse: bool) -> Element<
|
||||
};
|
||||
|
||||
crate::ui::dock::frame(
|
||||
column![title_bar, top, body, options_card(palette)].spacing(6),
|
||||
title_bar,
|
||||
column![top, body, options_card(palette)].spacing(6),
|
||||
width,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -100,13 +100,10 @@ pub fn view<'a>(
|
||||
document: &'a CadDocument,
|
||||
open_sketch: Option<&'a str>,
|
||||
width: f32,
|
||||
auto_collapse: bool,
|
||||
chrome: crate::ui::dock::Chrome,
|
||||
) -> Element<'a, Message> {
|
||||
let title_bar = crate::ui::dock::title_bar(
|
||||
PanelId::Browser,
|
||||
crate::t!("Browser").into_owned(),
|
||||
auto_collapse,
|
||||
);
|
||||
let title_bar =
|
||||
crate::ui::dock::title_bar(PanelId::Browser, crate::t!("Browser").into_owned(), chrome);
|
||||
|
||||
let mut tree = column![].spacing(1);
|
||||
|
||||
@@ -151,5 +148,5 @@ pub fn view<'a>(
|
||||
.width(Fill)
|
||||
.height(Fill);
|
||||
|
||||
crate::ui::dock::frame(column![title_bar, body].spacing(6), width)
|
||||
crate::ui::dock::frame(title_bar, column![body].spacing(6), width)
|
||||
}
|
||||
@@ -660,13 +660,10 @@ pub fn view<'a>(
|
||||
doc_id: u64,
|
||||
epoch: u64,
|
||||
width: f32,
|
||||
auto_collapse: bool,
|
||||
chrome: crate::ui::dock::Chrome,
|
||||
) -> Element<'a, Message> {
|
||||
let title_bar = crate::ui::dock::title_bar(
|
||||
PanelId::Count,
|
||||
crate::t!("Count").into_owned(),
|
||||
auto_collapse,
|
||||
);
|
||||
let title_bar =
|
||||
crate::ui::dock::title_bar(PanelId::Count, crate::t!("Count").into_owned(), chrome);
|
||||
let body = match mode {
|
||||
Some(mode) if mode.target.is_some() => mode_view(palette, mode, doc),
|
||||
_ => {
|
||||
@@ -685,7 +682,7 @@ pub fn view<'a>(
|
||||
list_view(palette, &instances)
|
||||
}
|
||||
};
|
||||
crate::ui::dock::frame(column![title_bar, body].spacing(6), width)
|
||||
crate::ui::dock::frame(title_bar, column![body].spacing(6), width)
|
||||
}
|
||||
|
||||
fn bar_button<'a>(
|
||||
|
||||
@@ -0,0 +1,498 @@
|
||||
//! Gradient flyout of the Graphic Attributes palette.
|
||||
|
||||
use crate::app::Message;
|
||||
use crate::scene::model::hatch_model::GradientKind;
|
||||
use crate::ui::window::graphic_attributes::GraphicAttributesMsg;
|
||||
use codec::types::Color as AcadColor;
|
||||
use iced::widget::{
|
||||
button, checkbox, column, container, image, mouse_area, row, slider, text, text_input, tooltip,
|
||||
Space,
|
||||
};
|
||||
use iced::{Background, Border, Color, Element, Fill, Length, Theme};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum GradientColorMode {
|
||||
One,
|
||||
Two,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GradientEditorState {
|
||||
pub handles: Vec<codec::Handle>,
|
||||
pub color_mode: GradientColorMode,
|
||||
pub color_1: AcadColor,
|
||||
pub color_2: AcadColor,
|
||||
pub shade_tint: f32,
|
||||
pub kind: GradientKind,
|
||||
pub inverted: bool,
|
||||
pub angle: String,
|
||||
pub centered: bool,
|
||||
/// Large preview, rebuilt only when a setting it depends on changes.
|
||||
preview: std::cell::RefCell<Option<(PreviewKey, Option<iced::widget::image::Handle>)>>,
|
||||
}
|
||||
|
||||
type PreviewKey = (GradientColorMode, AcadColor, AcadColor, u32, GradientKind, bool, String, bool);
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum GradientMsg {
|
||||
Open,
|
||||
Cancel,
|
||||
Apply,
|
||||
ColorMode(GradientColorMode),
|
||||
/// Stop colour 1 or 2.
|
||||
Color(u8, AcadColor),
|
||||
ShadeTint(f32),
|
||||
Kind(GradientKind),
|
||||
InvertToggled,
|
||||
Angle(String),
|
||||
AngleReset,
|
||||
Centered(bool),
|
||||
}
|
||||
|
||||
fn msg(message: GradientMsg) -> Message {
|
||||
Message::GraphicAttributes(GraphicAttributesMsg::Gradient(message))
|
||||
}
|
||||
|
||||
impl GradientEditorState {
|
||||
pub fn from_hatch(
|
||||
handles: Vec<codec::Handle>,
|
||||
hatch: &codec::entities::Hatch,
|
||||
) -> Self {
|
||||
let color = |index: usize| {
|
||||
hatch
|
||||
.gradient_color
|
||||
.colors
|
||||
.get(index)
|
||||
.map(|entry| entry.color)
|
||||
.unwrap_or(AcadColor::Index(7))
|
||||
};
|
||||
let (kind, inverted) = GradientKind::from_name(&hatch.gradient_color.name);
|
||||
Self {
|
||||
handles,
|
||||
color_mode: if hatch.gradient_color.is_single_color {
|
||||
GradientColorMode::One
|
||||
} else {
|
||||
GradientColorMode::Two
|
||||
},
|
||||
color_1: color(0),
|
||||
color_2: color(1),
|
||||
shade_tint: hatch.gradient_color.color_tint as f32,
|
||||
kind,
|
||||
inverted,
|
||||
angle: format!("{:.1}°", hatch.gradient_color.angle.to_degrees()),
|
||||
centered: hatch.gradient_color.shift.abs() < 1e-9,
|
||||
preview: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, message: GradientMsg) {
|
||||
match message {
|
||||
GradientMsg::ColorMode(mode) => self.color_mode = mode,
|
||||
GradientMsg::Color(1, color) => self.color_1 = color,
|
||||
GradientMsg::Color(_, color) => self.color_2 = color,
|
||||
GradientMsg::ShadeTint(value) => self.shade_tint = value.clamp(0.0, 1.0),
|
||||
GradientMsg::Kind(kind) => {
|
||||
self.kind = kind;
|
||||
if kind == GradientKind::Linear {
|
||||
self.inverted = false;
|
||||
}
|
||||
}
|
||||
GradientMsg::InvertToggled => {
|
||||
if self.kind != GradientKind::Linear {
|
||||
self.inverted ^= true;
|
||||
}
|
||||
}
|
||||
GradientMsg::Angle(value) => self.angle = value,
|
||||
GradientMsg::AngleReset => self.angle = "0.0°".into(),
|
||||
GradientMsg::Centered(value) => self.centered = value,
|
||||
GradientMsg::Open | GradientMsg::Cancel | GradientMsg::Apply => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn angle_radians(&self) -> f64 {
|
||||
self.angle
|
||||
.trim()
|
||||
.trim_end_matches('°')
|
||||
.replace(',', ".")
|
||||
.parse::<f64>()
|
||||
.unwrap_or(0.0)
|
||||
.rem_euclid(360.0)
|
||||
.to_radians()
|
||||
}
|
||||
}
|
||||
|
||||
fn rgb(color: &AcadColor) -> Color {
|
||||
let (r, g, b) = match color {
|
||||
AcadColor::Rgb { r, g, b } => (*r, *g, *b),
|
||||
AcadColor::Index(i) => {
|
||||
codec::types::aci_table::aci_to_rgb(*i).unwrap_or((128, 128, 128))
|
||||
}
|
||||
_ => (255, 255, 255),
|
||||
};
|
||||
Color::from_rgb8(r, g, b)
|
||||
}
|
||||
|
||||
fn rgb_array(color: &AcadColor) -> [f32; 4] {
|
||||
let c = rgb(color);
|
||||
[c.r, c.g, c.b, 1.0]
|
||||
}
|
||||
|
||||
fn mix_rgba(a: Color, b: Color, t: f32) -> [u8; 4] {
|
||||
let channel = |a: f32, b: f32| ((a + (b - a) * t) * 255.0).round() as u8;
|
||||
[channel(a.r, b.r), channel(a.g, b.g), channel(a.b, b.b), 255]
|
||||
}
|
||||
|
||||
fn preview_sized(
|
||||
state: &GradientEditorState,
|
||||
width: u32,
|
||||
height: u32,
|
||||
) -> Option<iced::widget::image::Handle> {
|
||||
let c1 = rgb(&state.color_1);
|
||||
let c2 = if state.color_mode == GradientColorMode::One {
|
||||
let c = crate::scene::gradient_tint_color(rgb_array(&state.color_1), state.shade_tint);
|
||||
Color::from_rgb(c[0], c[1], c[2])
|
||||
} else {
|
||||
rgb(&state.color_2)
|
||||
};
|
||||
let angle = state.angle_radians();
|
||||
let frame = kernel::geom2d::gradient_frame(
|
||||
&[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]],
|
||||
angle,
|
||||
if state.centered { 0.0 } else { 1.0 },
|
||||
kernel::geom2d::Tolerance::default(),
|
||||
)?;
|
||||
let (sin, cos) = angle.sin_cos();
|
||||
let radial = state.kind.radial();
|
||||
let mut pixels = Vec::with_capacity((width * height * 4) as usize);
|
||||
for y in 0..height {
|
||||
let py = y as f64 / (height - 1) as f64;
|
||||
for x in 0..width {
|
||||
let px = x as f64 / (width - 1) as f64;
|
||||
let mut t = if radial {
|
||||
((px - frame.center[0]).hypot(py - frame.center[1]) / frame.radius).clamp(0.0, 1.0)
|
||||
as f32
|
||||
} else {
|
||||
(((px * cos + py * sin) - frame.projection_min) / frame.projection_span)
|
||||
.clamp(0.0, 1.0) as f32
|
||||
};
|
||||
if state.kind == GradientKind::Cylinder {
|
||||
t = 1.0 - (2.0 * t - 1.0).abs();
|
||||
} else if state.kind == GradientKind::Curved {
|
||||
t *= t;
|
||||
} else if state.kind == GradientKind::Hemispherical {
|
||||
t = t.sqrt();
|
||||
}
|
||||
if state.inverted {
|
||||
t = 1.0 - t;
|
||||
}
|
||||
pixels.extend_from_slice(&if radial {
|
||||
mix_rgba(c2, c1, t)
|
||||
} else {
|
||||
mix_rgba(c1, c2, t)
|
||||
});
|
||||
}
|
||||
}
|
||||
Some(iced::widget::image::Handle::from_rgba(width, height, pixels))
|
||||
}
|
||||
|
||||
fn preview(state: &GradientEditorState) -> Element<'static, Message> {
|
||||
let key: PreviewKey = (
|
||||
state.color_mode,
|
||||
state.color_1,
|
||||
state.color_2,
|
||||
state.shade_tint.to_bits(),
|
||||
state.kind,
|
||||
state.inverted,
|
||||
state.angle.clone(),
|
||||
state.centered,
|
||||
);
|
||||
let mut cache = state.preview.borrow_mut();
|
||||
if cache.as_ref().is_none_or(|(cached, _)| *cached != key) {
|
||||
*cache = Some((key, preview_sized(state, 370, 80)));
|
||||
}
|
||||
preview_image(cache.as_ref().and_then(|(_, handle)| handle.clone()))
|
||||
}
|
||||
|
||||
pub(crate) fn compact_preview(
|
||||
hatch: &codec::entities::Hatch,
|
||||
) -> Option<iced::widget::image::Handle> {
|
||||
preview_sized(&GradientEditorState::from_hatch(Vec::new(), hatch), 256, 26)
|
||||
}
|
||||
|
||||
pub(crate) fn preview_image(handle: Option<iced::widget::image::Handle>) -> Element<'static, Message> {
|
||||
match handle {
|
||||
Some(handle) => image(handle)
|
||||
.width(Fill)
|
||||
.height(Fill)
|
||||
.content_fit(iced::ContentFit::Fill)
|
||||
.into(),
|
||||
None => Space::new().width(Fill).height(Fill).into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn icon(kind: GradientKind, inverted: bool) -> &'static [u8] {
|
||||
match (kind, inverted) {
|
||||
(GradientKind::Linear, _) => {
|
||||
include_bytes!("../../../assets/icons/gradient/gradient_linear.svg")
|
||||
}
|
||||
(GradientKind::Cylinder, false) => {
|
||||
include_bytes!("../../../assets/icons/gradient/gradient_cylindrical.svg")
|
||||
}
|
||||
(GradientKind::Cylinder, true) => {
|
||||
include_bytes!("../../../assets/icons/gradient/gradient_cylindrical_inverted.svg")
|
||||
}
|
||||
(GradientKind::Spherical, false) => {
|
||||
include_bytes!("../../../assets/icons/gradient/gradient_spherical.svg")
|
||||
}
|
||||
(GradientKind::Spherical, true) => {
|
||||
include_bytes!("../../../assets/icons/gradient/gradient_spherical_inverted.svg")
|
||||
}
|
||||
(GradientKind::Hemispherical, false) => {
|
||||
include_bytes!("../../../assets/icons/gradient/gradient_hemispherical.svg")
|
||||
}
|
||||
(GradientKind::Hemispherical, true) => {
|
||||
include_bytes!("../../../assets/icons/gradient/gradient_hemispherical_inverted.svg")
|
||||
}
|
||||
(GradientKind::Curved, false) => {
|
||||
include_bytes!("../../../assets/icons/gradient/gradient_curved.svg")
|
||||
}
|
||||
(GradientKind::Curved, true) => {
|
||||
include_bytes!("../../../assets/icons/gradient/gradient_curved_inverted.svg")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view(state: &GradientEditorState, dimmed: bool) -> Element<'_, Message> {
|
||||
let segment = |label, mode| {
|
||||
let selected = state.color_mode == mode;
|
||||
button(text(label).size(11))
|
||||
.on_press(msg(GradientMsg::ColorMode(mode)))
|
||||
.width(Fill)
|
||||
.style(move |theme: &Theme, status| {
|
||||
if selected {
|
||||
button::primary(theme, status)
|
||||
} else {
|
||||
button::secondary(theme, status)
|
||||
}
|
||||
})
|
||||
};
|
||||
let color_field = |label: std::borrow::Cow<'static, str>, index, color: AcadColor| {
|
||||
let (background, _) = crate::ui::properties::acad_color_display(color);
|
||||
let value = crate::ui::color_select::color_display_name(color);
|
||||
let picker = button(
|
||||
row![
|
||||
crate::ui::color_select::swatch(background),
|
||||
text(value).size(10),
|
||||
Space::new().width(Fill),
|
||||
]
|
||||
.spacing(5)
|
||||
.align_y(iced::Center),
|
||||
)
|
||||
.on_press(Message::OpenColorWindow(
|
||||
crate::app::ColorPickTarget::Gradient(index),
|
||||
color,
|
||||
))
|
||||
.style(|theme: &Theme, status| {
|
||||
let palette = theme.palette();
|
||||
button::Style {
|
||||
background: Some(Background::Color(
|
||||
if matches!(status, button::Status::Hovered) {
|
||||
palette.background.weak.color
|
||||
} else {
|
||||
palette.background.base.color
|
||||
},
|
||||
)),
|
||||
text_color: palette.background.base.text,
|
||||
border: Border {
|
||||
color: palette.background.neutral.color,
|
||||
width: 1.0,
|
||||
radius: 2.0.into(),
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
})
|
||||
.height(crate::ui::ROW_H)
|
||||
.padding([3, 6])
|
||||
.width(Fill);
|
||||
row![text(label).size(11).width(43), picker]
|
||||
.spacing(4)
|
||||
.align_y(iced::Center)
|
||||
.width(Fill)
|
||||
};
|
||||
let kinds = [
|
||||
(GradientKind::Linear, crate::t!("Linear")),
|
||||
(GradientKind::Cylinder, crate::t!("Cyl.")),
|
||||
(GradientKind::Spherical, crate::t!("Spher.")),
|
||||
(GradientKind::Hemispherical, crate::t!("Hemi.")),
|
||||
(GradientKind::Curved, crate::t!("Curved")),
|
||||
];
|
||||
let mut type_row = row![].spacing(3);
|
||||
for (kind, label) in kinds {
|
||||
let selected = state.kind == kind;
|
||||
let b = button(
|
||||
column![
|
||||
crate::ui::icons::semantic(
|
||||
icon(kind, state.inverted && kind != GradientKind::Linear),
|
||||
25.0
|
||||
),
|
||||
text(label).size(9)
|
||||
]
|
||||
.align_x(iced::Center),
|
||||
)
|
||||
.on_press(msg(GradientMsg::Kind(kind)))
|
||||
.width(48)
|
||||
.height(48)
|
||||
.style(move |theme: &Theme, status| {
|
||||
let mut s = button::subtle(theme, status);
|
||||
if selected {
|
||||
s.border.color = theme.palette().primary.base.color;
|
||||
s.border.width = 1.0;
|
||||
}
|
||||
s
|
||||
});
|
||||
type_row = type_row.push(tooltip(
|
||||
b,
|
||||
text(kind.choice_label(false)).size(10),
|
||||
tooltip::Position::Bottom,
|
||||
));
|
||||
}
|
||||
let invert_selected = state.inverted && state.kind != GradientKind::Linear;
|
||||
let invert = button(column![text("⇄").size(18), text(crate::t!("Invert")).size(9)].align_x(iced::Center))
|
||||
.on_press_maybe(
|
||||
(state.kind != GradientKind::Linear).then(|| msg(GradientMsg::InvertToggled)),
|
||||
)
|
||||
.width(42)
|
||||
.height(48)
|
||||
.style(move |theme: &Theme, status| {
|
||||
let mut style = button::secondary(theme, status);
|
||||
if invert_selected {
|
||||
style.border.color = theme.palette().primary.base.color;
|
||||
style.border.width = 1.0;
|
||||
}
|
||||
style
|
||||
});
|
||||
let separator =
|
||||
container(Space::new().width(1).height(36)).style(|theme: &Theme| container::Style {
|
||||
background: Some(Background::Color(theme.palette().background.neutral.color)),
|
||||
..Default::default()
|
||||
});
|
||||
type_row = type_row.push(separator).push(tooltip(
|
||||
invert,
|
||||
text(crate::t!("Invert gradient")).size(10),
|
||||
tooltip::Position::Bottom,
|
||||
));
|
||||
let colors: Element<'_, Message> = if state.color_mode == GradientColorMode::Two {
|
||||
row![
|
||||
color_field(crate::t!("Color 1"), 1, state.color_1),
|
||||
color_field(crate::t!("Color 2"), 2, state.color_2)
|
||||
]
|
||||
.spacing(6)
|
||||
.into()
|
||||
} else {
|
||||
row![
|
||||
color_field(crate::t!("Color 1"), 1, state.color_1),
|
||||
row![
|
||||
text(crate::t!("Tone")).size(11).width(43),
|
||||
slider(0..=100, (state.shade_tint * 100.0) as i32, |v| {
|
||||
msg(GradientMsg::ShadeTint(v as f32 / 100.0))
|
||||
})
|
||||
.width(Fill)
|
||||
]
|
||||
.spacing(4)
|
||||
.align_y(iced::Center)
|
||||
.height(crate::ui::ROW_H)
|
||||
.width(Fill)
|
||||
]
|
||||
.spacing(6)
|
||||
.align_y(iced::Center)
|
||||
.into()
|
||||
};
|
||||
let angle = row![
|
||||
text(crate::t!("Angle")).size(11).width(43),
|
||||
text_input("0.0°", &state.angle)
|
||||
.on_input(|value| msg(GradientMsg::Angle(value)))
|
||||
.size(11)
|
||||
.width(Length::Fixed(92.0)),
|
||||
tooltip(
|
||||
button(crate::ui::icons::themed_undo(11.0, true))
|
||||
.on_press(msg(GradientMsg::AngleReset))
|
||||
.style(button::subtle)
|
||||
.padding(3)
|
||||
.width(24)
|
||||
.height(crate::ui::ROW_H),
|
||||
text(crate::t!("Reset angle")).size(10),
|
||||
tooltip::Position::Bottom,
|
||||
),
|
||||
Space::new().width(Fill),
|
||||
checkbox(state.centered)
|
||||
.label(crate::t!("Centered"))
|
||||
.on_toggle(|value| msg(GradientMsg::Centered(value)))
|
||||
.size(14)
|
||||
.text_size(11)
|
||||
]
|
||||
.spacing(6)
|
||||
.align_y(iced::Center);
|
||||
let types = row![text(crate::t!("Type")).size(11).width(43), type_row]
|
||||
.spacing(4)
|
||||
.align_y(iced::Center);
|
||||
let actions = container(
|
||||
row![
|
||||
button(text(crate::t!("Cancel")).size(11))
|
||||
.on_press(msg(GradientMsg::Cancel))
|
||||
.style(button::secondary)
|
||||
.padding([5, 14]),
|
||||
button(text(crate::t!("OK")).size(11))
|
||||
.on_press(msg(GradientMsg::Apply))
|
||||
.style(button::primary)
|
||||
.padding([5, 18]),
|
||||
]
|
||||
.spacing(8),
|
||||
)
|
||||
.width(Fill)
|
||||
.align_x(iced::alignment::Horizontal::Right);
|
||||
let content = column![
|
||||
container(preview(state)).width(Fill).height(80),
|
||||
row![
|
||||
segment(crate::t!("One Color"), GradientColorMode::One),
|
||||
segment(crate::t!("Two Colors"), GradientColorMode::Two)
|
||||
]
|
||||
.spacing(2),
|
||||
colors,
|
||||
types,
|
||||
angle,
|
||||
actions
|
||||
]
|
||||
.spacing(8)
|
||||
.padding(10);
|
||||
let panel: Element<'_, Message> = container(content)
|
||||
.width(Length::Fixed(390.0))
|
||||
.style(|theme: &Theme| container::Style {
|
||||
background: Some(Background::Color(theme.palette().background.base.color)),
|
||||
border: Border {
|
||||
color: theme.palette().primary.base.color,
|
||||
width: 1.0,
|
||||
radius: 3.0.into(),
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.into();
|
||||
let panel: Element<'_, Message> = if dimmed {
|
||||
let shield = container(Space::new())
|
||||
.width(Fill)
|
||||
.height(Fill)
|
||||
.style(|theme: &Theme| container::Style {
|
||||
background: Some(Background::Color(
|
||||
theme.palette().background.strongest.color.scale_alpha(0.55),
|
||||
)),
|
||||
..Default::default()
|
||||
});
|
||||
iced::widget::stack![panel, iced::widget::opaque(shield)]
|
||||
.width(Length::Fixed(390.0))
|
||||
.into()
|
||||
} else {
|
||||
panel
|
||||
};
|
||||
mouse_area(panel)
|
||||
.interaction(iced::mouse::Interaction::Pointer)
|
||||
.into()
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
+53
-23
@@ -337,20 +337,40 @@ impl LayerPanel {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Render the layer panel as the full content of its own OS window.
|
||||
pub fn view_window(
|
||||
/// The Layer Manager as a dock pallet: its title bar, the toolbar across
|
||||
/// the pallet (search box on the right), then the table, at least as wide
|
||||
/// as the pallet and scrolling sideways when the pallet is narrower.
|
||||
pub fn view_panel(
|
||||
&self,
|
||||
name_col_w: f32,
|
||||
sizing: crate::ui::modal::ModalSizing,
|
||||
width: f32,
|
||||
chrome: crate::ui::dock::Chrome,
|
||||
) -> Element<'_, Message> {
|
||||
self.view_content(name_col_w, sizing)
|
||||
use crate::ui::dock::PanelId;
|
||||
let title_bar = crate::ui::dock::title_bar(
|
||||
PanelId::Layers,
|
||||
crate::tr!("modal", "layer-manager"),
|
||||
chrome,
|
||||
);
|
||||
// Inside the pallet's 1 px border on either side.
|
||||
let body = self.view_content(name_col_w, (width - 2.0).max(0.0));
|
||||
container(column![title_bar, body])
|
||||
.width(Length::Fixed(width))
|
||||
.height(Length::Fill)
|
||||
.style(|theme: &Theme| container::Style {
|
||||
background: Some(Background::Color(theme.palette().background.base.color)),
|
||||
border: Border {
|
||||
color: theme.palette().background.neutral.color,
|
||||
width: 1.0,
|
||||
radius: 0.0.into(),
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
fn view_content(
|
||||
&self,
|
||||
name_col_w: f32,
|
||||
sizing: crate::ui::modal::ModalSizing,
|
||||
) -> Element<'_, Message> {
|
||||
/// Toolbar and table for a pallet `inner_w` wide.
|
||||
fn view_content(&self, name_col_w: f32, inner_w: f32) -> Element<'_, Message> {
|
||||
let has_sel = self.selected.is_some();
|
||||
let sel_is_zero = self
|
||||
.selected
|
||||
@@ -377,7 +397,7 @@ impl LayerPanel {
|
||||
Message::LayerSetCurrent,
|
||||
can_set_current,
|
||||
),
|
||||
iced::widget::Space::new().width(sizing.width),
|
||||
iced::widget::Space::new().width(Length::Fill),
|
||||
// Search box: filters rows by name as the user types (#343).
|
||||
text_input(t!("Search…").as_ref(), &self.filter)
|
||||
.on_input(Message::LayerManagerFilterChanged)
|
||||
@@ -393,7 +413,7 @@ impl LayerPanel {
|
||||
background: Some(Background::Color(theme.palette().background.weak.color)),
|
||||
..Default::default()
|
||||
})
|
||||
.width(sizing.width)
|
||||
.width(Length::Fill)
|
||||
.padding([4, 8]);
|
||||
|
||||
// ── Column header ─────────────────────────────────────────────────
|
||||
@@ -473,7 +493,6 @@ impl LayerPanel {
|
||||
),
|
||||
]
|
||||
.spacing(4)
|
||||
.width(sizing.width)
|
||||
.align_y(iced::Center);
|
||||
|
||||
for vp in &self.vp_cols {
|
||||
@@ -499,7 +518,7 @@ impl LayerPanel {
|
||||
}
|
||||
})
|
||||
.padding([4, 8])
|
||||
.width(sizing.width);
|
||||
.width(Length::Fill);
|
||||
|
||||
// ── Layer rows ────────────────────────────────────────────────────
|
||||
let mut rows_col = column![].spacing(0);
|
||||
@@ -536,17 +555,28 @@ impl LayerPanel {
|
||||
|
||||
let table = scrollable(rows_col)
|
||||
.id(iced::advanced::widget::Id::new(LAYER_TABLE_SCROLL_ID))
|
||||
.height(sizing.height.min(240.0));
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill);
|
||||
|
||||
// ── Full-window frame ─────────────────────────────────────────────
|
||||
container(column![toolbar, col_header, table].spacing(0))
|
||||
.style(|theme: &Theme| container::Style {
|
||||
background: Some(Background::Color(theme.palette().background.base.color)),
|
||||
..Default::default()
|
||||
})
|
||||
.width(sizing.width)
|
||||
.height(sizing.height)
|
||||
.into()
|
||||
// The header and table stretch to the wider of their columns and the
|
||||
// pallet (the zero-height spacer), so the header's grey band and
|
||||
// borders always run to the pallet's edge; a narrower pallet scrolls
|
||||
// the table sideways while the toolbar stays put.
|
||||
let grid = column![
|
||||
col_header,
|
||||
table,
|
||||
iced::widget::Space::new()
|
||||
.width(Length::Fixed(inner_w))
|
||||
.height(0),
|
||||
]
|
||||
.height(Length::Fill);
|
||||
let grid = scrollable(grid)
|
||||
.direction(iced::widget::scrollable::Direction::Horizontal(
|
||||
iced::widget::scrollable::Scrollbar::new(),
|
||||
))
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill);
|
||||
column![toolbar, grid].height(Length::Fill).into()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,15 @@ pub fn view_window<'a>(
|
||||
))
|
||||
.size(11)
|
||||
.style(muted_style),
|
||||
container(scrollable(column(listed).spacing(2)).height(content_height))
|
||||
// The right padding keeps the names clear of the scrollbar, which
|
||||
// a list as wide as its longest name would otherwise draw over.
|
||||
container(
|
||||
scrollable(column(listed).spacing(2).padding(iced::Padding {
|
||||
right: 14.0,
|
||||
..iced::Padding::ZERO
|
||||
}))
|
||||
.height(content_height),
|
||||
)
|
||||
.padding([10, 12])
|
||||
.width(content_width)
|
||||
.height(content_height)
|
||||
|
||||
@@ -13,6 +13,8 @@ pub mod drawing_units;
|
||||
pub mod field_dialog;
|
||||
pub mod find_replace;
|
||||
pub mod geometric_tolerance;
|
||||
pub mod gradient_editor;
|
||||
pub mod graphic_attributes;
|
||||
pub mod layer_state_manager;
|
||||
pub mod layer_translator;
|
||||
pub mod layers;
|
||||
|
||||
@@ -277,12 +277,12 @@ pub fn view<'a>(
|
||||
document: &'a CadDocument,
|
||||
selected: &[Handle],
|
||||
width: f32,
|
||||
auto_collapse: bool,
|
||||
chrome: crate::ui::dock::Chrome,
|
||||
) -> Element<'a, Message> {
|
||||
let title_bar = crate::ui::dock::title_bar(
|
||||
PanelId::PointCloudManager,
|
||||
crate::t!("Point Cloud Manager").into_owned(),
|
||||
auto_collapse,
|
||||
chrome,
|
||||
);
|
||||
let query = state.search.trim().to_lowercase();
|
||||
let matches = |label: &str| query.is_empty() || label.to_lowercase().contains(&query);
|
||||
@@ -416,5 +416,5 @@ pub fn view<'a>(
|
||||
.padding([4, 8])
|
||||
.size(12);
|
||||
|
||||
crate::ui::dock::frame(column![title_bar, card, search].spacing(6), width)
|
||||
crate::ui::dock::frame(title_bar, column![card, search].spacing(6), width)
|
||||
}
|
||||
+61
-10
@@ -3,6 +3,8 @@
|
||||
//! quality and output options into one dialog; on commit it either sends the
|
||||
//! current layout to a system printer (with the chosen options) or writes a
|
||||
//! PDF. Styled to match the other OCS dialogs (dark pills + fields).
|
||||
//! The web build has no system printers, so PDF is the only output it
|
||||
//! offers — and its default.
|
||||
|
||||
use crate::app::Message;
|
||||
use crate::io::paper_catalog::{self, CustomPaper, Margins, PaperSize, PaperUnits};
|
||||
@@ -21,6 +23,7 @@ use std::fmt;
|
||||
/// Sentinel entries in the printer dropdown (not real printer names).
|
||||
pub const OUT_DEFAULT: &str = "System default printer";
|
||||
pub const OUT_PDF: &str = "Save to PDF file…";
|
||||
pub const OUT_SVG: &str = "Save to SVG file…";
|
||||
|
||||
/// Top-of-list entries: no page setup (defaults + PDF), and the last-used
|
||||
/// settings captured when the dialog opened.
|
||||
@@ -483,8 +486,13 @@ pub struct PlotDialogState {
|
||||
pub page_setup_on_new_layout: bool,
|
||||
/// Chosen printer name, or `None` for the system default.
|
||||
pub printer: Option<String>,
|
||||
/// Output goes to a PDF file instead of a printer.
|
||||
/// Output goes to a file instead of a printer.
|
||||
pub to_file: bool,
|
||||
/// File output is SVG instead of PDF (only meaningful when `to_file`).
|
||||
/// Session state, like the printer choice: never written into the
|
||||
/// drawing's page setup, which only knows printers and PDF drivers.
|
||||
#[serde(default)]
|
||||
pub file_svg: bool,
|
||||
pub paper: String,
|
||||
#[serde(skip)]
|
||||
pub paper_width_mm: f64,
|
||||
@@ -593,7 +601,9 @@ impl Default for PlotDialogState {
|
||||
import_draft: None,
|
||||
page_setup_on_new_layout: false,
|
||||
printer: None,
|
||||
to_file: false,
|
||||
// Web defaults to PDF output: it has no system printers.
|
||||
to_file: cfg!(target_arch = "wasm32"),
|
||||
file_svg: false,
|
||||
paper: paper_catalog::default_paper().canonical.to_string(),
|
||||
paper_width_mm: 297.0,
|
||||
paper_height_mm: 210.0,
|
||||
@@ -650,6 +660,7 @@ impl PlotDialogState {
|
||||
pub fn copy_settings_from(&mut self, o: &PlotDialogState) {
|
||||
self.printer = o.printer.clone();
|
||||
self.to_file = o.to_file;
|
||||
self.file_svg = o.file_svg;
|
||||
self.paper = o.paper.clone();
|
||||
self.paper_width_mm = o.paper_width_mm;
|
||||
self.paper_height_mm = o.paper_height_mm;
|
||||
@@ -1136,6 +1147,25 @@ fn custom_paper_editor<'a>(draft: &'a CustomPaperDraft, width: Length) -> Elemen
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Entries for the output dropdown. The web build has no system printers, so
|
||||
/// PDF is the only output it offers; native lists the system default, every
|
||||
/// discovered printer, then PDF.
|
||||
fn output_choices(printers: &[String]) -> Vec<PlotChoice> {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
let _ = printers;
|
||||
vec![PlotChoice::localized(OUT_PDF)]
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let mut out = vec![PlotChoice::localized(OUT_DEFAULT)];
|
||||
out.extend(printers.iter().cloned().map(PlotChoice::raw));
|
||||
out.push(PlotChoice::localized(OUT_PDF));
|
||||
out.push(PlotChoice::localized(OUT_SVG));
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view_window(
|
||||
s: &PlotDialogState,
|
||||
print_all_options: bool,
|
||||
@@ -1143,10 +1173,21 @@ pub fn view_window(
|
||||
) -> Element<'_, Message> {
|
||||
let width = sizing.width;
|
||||
let height = sizing.height;
|
||||
// The web build always outputs PDF, whatever a stored dialog state says;
|
||||
// native follows the stored choice.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let to_file = true;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let to_file = s.to_file;
|
||||
let action = if print_all_options {
|
||||
t!("Apply")
|
||||
} else if s.to_file {
|
||||
t!("Export PDF")
|
||||
} else if to_file {
|
||||
// The web build writes PDF whatever a stored dialog state says.
|
||||
if s.file_svg && cfg!(not(target_arch = "wasm32")) {
|
||||
t!("Export SVG")
|
||||
} else {
|
||||
t!("Export PDF")
|
||||
}
|
||||
} else {
|
||||
t!("Print")
|
||||
};
|
||||
@@ -1277,11 +1318,21 @@ pub fn view_window(
|
||||
},
|
||||
None => PlotChoice::localized(OUT_DEFAULT),
|
||||
};
|
||||
let mut printer_opts = vec![default_entry.clone()];
|
||||
printer_opts.extend(s.printers.iter().cloned().map(PlotChoice::raw));
|
||||
printer_opts.push(PlotChoice::localized(OUT_PDF));
|
||||
let printer_sel = if s.to_file {
|
||||
Some(PlotChoice::localized(OUT_PDF))
|
||||
let mut printer_opts = output_choices(&s.printers);
|
||||
if let Some(first) = printer_opts
|
||||
.first_mut()
|
||||
.filter(|first| first.raw == OUT_DEFAULT)
|
||||
{
|
||||
*first = default_entry.clone();
|
||||
}
|
||||
let printer_sel = if to_file {
|
||||
Some(PlotChoice::localized(
|
||||
if s.file_svg && cfg!(not(target_arch = "wasm32")) {
|
||||
OUT_SVG
|
||||
} else {
|
||||
OUT_PDF
|
||||
},
|
||||
))
|
||||
} else {
|
||||
Some(match &s.printer {
|
||||
Some(printer) => PlotChoice::raw(printer.clone()),
|
||||
@@ -1360,7 +1411,7 @@ pub fn view_window(
|
||||
.spacing(8)
|
||||
.align_y(iced::Center)
|
||||
.width(Length::Fill);
|
||||
let printer_controls: Element<'_, Message> = if s.to_file {
|
||||
let printer_controls: Element<'_, Message> = if to_file {
|
||||
Space::new().height(0).into()
|
||||
} else {
|
||||
row![
|
||||
|
||||
@@ -1153,12 +1153,12 @@ fn heading(label: &str) -> Element<'static, Message> {
|
||||
pub fn view<'a>(
|
||||
state: &'a SheetSetManager,
|
||||
width: f32,
|
||||
auto_collapse: bool,
|
||||
chrome: crate::ui::dock::Chrome,
|
||||
) -> Element<'a, Message> {
|
||||
let title_bar = crate::ui::dock::title_bar(
|
||||
PanelId::SheetSetManager,
|
||||
t!("Sheet Set Manager").into_owned(),
|
||||
auto_collapse,
|
||||
chrome,
|
||||
);
|
||||
let mut picks: Vec<Pick> = state
|
||||
.sets
|
||||
@@ -1305,7 +1305,8 @@ pub fn view<'a>(
|
||||
.size(10)
|
||||
.style(muted_style);
|
||||
crate::ui::dock::frame(
|
||||
column![title_bar, combo, tabs, card, hint].spacing(6),
|
||||
title_bar,
|
||||
column![combo, tabs, card, hint].spacing(6),
|
||||
width,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -590,7 +590,7 @@ impl XrefManagerPanel {
|
||||
pub fn view<'a>(
|
||||
&'a self,
|
||||
width: f32,
|
||||
auto_collapse: bool,
|
||||
chrome: crate::ui::dock::Chrome,
|
||||
missing: usize,
|
||||
doc: &'a CadDocument,
|
||||
) -> Element<'a, Message> {
|
||||
@@ -602,7 +602,7 @@ impl XrefManagerPanel {
|
||||
crate::t!("External References").as_ref(),
|
||||
self.entries.len()
|
||||
),
|
||||
auto_collapse,
|
||||
chrome,
|
||||
);
|
||||
// Table content width: column widths plus gutters, stretched to the
|
||||
// dock when wider so rows fill the panel; narrower docks sidescroll.
|
||||
|
||||
Reference in new issue
Block a user