Merge pull request #1486 from ramox81/codex/xref-context-tab

Selection tools on the right-edge toolbar for PDF underlays and xrefs
This commit is contained in:
Hakan Seven committed 2026-09-25 15:18:33 +03:00
commit 82fe204a70
34 files changed
+387 -353

No files matched your search

+98
View File
@@ -0,0 +1,98 @@
//! Selection-driven tools: while only PDF underlays (or only xrefs) are
//! selected their actions show on the right-edge toolbar; they go when the
//! selection changes. Contrast and fade stay in the Properties panel.
use super::*;
use crate::modules::{ModuleEvent, ToolDef};
/// The switches of the selected underlay (the first one) the toolbar shows
/// as on.
#[derive(Clone, Debug, PartialEq)]
pub struct UnderlayContext {
pub monochrome: bool,
pub shown: bool,
pub snap: bool,
}
fn tool(id: &'static str, label: &'static str, icon: &'static [u8]) -> ToolDef {
ToolDef {
id,
label,
icon: IconKind::Svg(icon),
event: ModuleEvent::Command(id.to_string()),
}
}
const CLIP: &[u8] = include_bytes!("../../../assets/icons/xclip.svg");
/// The clip icon with a small red cross at its lower right.
const UNCLIP: &[u8] = include_bytes!("../../../assets/icons/xclip_remove.svg");
/// PDF underlay tools: display, clipping, options, layers and import.
pub fn pdf_underlay_tools() -> Vec<ToolDef> {
const FRAMES: &[u8] = include_bytes!("../../../assets/icons/underlay_frames.svg");
vec![
tool("_PDFULMONO", "Display in\nMonochrome", FRAMES),
tool("_PDFULCLIP", "Create Clipping\nBoundary", CLIP),
tool("_PDFULUNCLIP", "Remove\nClipping", UNCLIP),
tool("_PDFULSHOW", "Show\nUnderlay", FRAMES),
tool(
"_PDFULSNAP",
"Enable\nSnap",
include_bytes!("../../../assets/icons/snap_underlays.svg"),
),
tool("EXTERNALREFERENCES", "External\nReferences", crate::ui::icons::FOLDER_OPEN),
tool(
"ULAYERS",
"Edit\nLayers",
include_bytes!("../../../assets/icons/underlay_layers.svg"),
),
tool(
"_PDFULIMPORT",
"Import As\nObjects",
include_bytes!("../../../assets/icons/cui_import.svg"),
),
]
}
/// External reference tools: edit, open, clipping and the palette.
pub fn xref_tools() -> Vec<ToolDef> {
vec![
tool(
"_XREFEDIT",
"Edit Reference\nIn-Place",
include_bytes!("../../../assets/icons/edit_block.svg"),
),
tool("_XREFOPEN", "Open\nReference", crate::ui::icons::FOLDER_OPEN),
tool("_XREFCLIP", "Create Clipping\nBoundary", CLIP),
tool("_XREFUNCLIP", "Remove\nClipping", UNCLIP),
tool("EXTERNALREFERENCES", "External\nReferences", crate::ui::icons::FOLDER_OPEN),
]
}
impl Ribbon {
/// The selection context: the selected underlay's switches, or that
/// only xrefs are selected, or neither.
pub fn set_underlay_context(&mut self, context: Option<UnderlayContext>, xref: bool) {
self.underlay_ctx = context;
self.xref_ctx = xref;
}
pub fn underlay_context(&self) -> Option<&UnderlayContext> {
self.underlay_ctx.as_ref()
}
pub fn xref_context(&self) -> bool {
self.xref_ctx
}
/// Select a tab by its module id.
pub fn select_by_id(&mut self, id: &str) -> bool {
match self.modules.iter().position(|m| m.id() == id) {
Some(index) => {
self.select(index);
true
}
None => false,
}
}
}
+11 -50
View File
@@ -25,8 +25,8 @@ mod draw_panel;
pub(crate) use draw_panel::tools as panel_tools;
mod modify_panel;
mod color_dropdown;
mod underlay_tab;
pub use underlay_tab::{UnderlayContext, UnderlayTabMsg};
mod context_tools;
pub use context_tools::{pdf_underlay_tools, xref_tools, UnderlayContext};
use widgets::{StyleContext, *};
pub(crate) use widgets::{REDO_HISTORY_ID, UNDO_HISTORY_ID};
mod collapse;
@@ -105,10 +105,10 @@ pub struct Ribbon {
/// Set by `CollapsePanels` when the tool row is in its tight state; the mode
/// selector hides itself then to give the cramped tab row its space back.
collapse_tight: Arc<AtomicBool>,
/// The selected PDF underlay's values while the contextual tab exists.
/// The selected PDF underlay's switches while only underlays are selected.
underlay_ctx: Option<UnderlayContext>,
/// The contextual underlay tab is the one shown.
underlay_tab_active: bool,
/// Only xrefs are selected.
xref_ctx: bool,
/// XDWGFADECTL as the Reference slide-out shows it (negative = off).
pub xref_fade: i32,
}
@@ -205,7 +205,7 @@ impl Ribbon {
collapse_mode: CollapseMode::default(),
collapse_tight: Arc::new(AtomicBool::new(false)),
underlay_ctx: None,
underlay_tab_active: false,
xref_ctx: false,
xref_fade: 50,
}
}
@@ -284,7 +284,6 @@ impl Ribbon {
pub fn select(&mut self, index: usize) {
if index < self.modules.len() {
self.active = index;
self.underlay_tab_active = false;
}
}
@@ -348,9 +347,6 @@ impl Ribbon {
show_block_palette,
show_file_tabs: self.show_file_tabs,
show_layout_tabs: self.show_layout_tabs,
underlay_mono: self.underlay_ctx.as_ref().is_some_and(|c| c.monochrome),
underlay_shown: self.underlay_ctx.as_ref().is_some_and(|c| c.shown),
underlay_snap: self.underlay_ctx.as_ref().is_some_and(|c| c.snap),
}
}
@@ -518,7 +514,7 @@ impl Ribbon {
return acc;
}
let is_active = i == self.active && !self.underlay_tab_shown();
let is_active = i == self.active;
let is_contextual = module.id() == "layout";
let btn = container(
button(text(crate::i18n::ribbon_module_title(module.id(), module.title())).size(12))
@@ -588,10 +584,6 @@ impl Ribbon {
},
);
let mut tab_items = tab_items;
if self.underlay_ctx.is_some() {
tab_items.push(underlay_tab_button(self.underlay_tab_shown()));
}
// Tabs may squeeze their gaps to fit before wrapping: from the normal 6px
// down to -12px on a narrow (e.g. phone) tab row, tucking neighbours into
@@ -673,14 +665,10 @@ impl Ribbon {
// shrink to compact icon columns, then it collapses to a ▾ flyout
// button. See `CollapsePanels`.
let ts = self.toggle_state(show_block_palette);
let panels: Vec<Panel<'_>> = if self.underlay_tab_shown() {
self.underlay_panels(ts, &style_ctx)
} else {
groups
.iter()
.map(|g| self.panel(g, ts, &style_ctx, &|_| Vec::new()))
.collect()
};
let panels: Vec<Panel<'_>> = groups
.iter()
.map(|g| self.panel(g, ts, &style_ctx, &|_| Vec::new()))
.collect();
CollapsePanels::new(panels, self.collapsed_open.clone(), TOOL_BAR_H)
.report_height(self.tool_bar_h.clone())
.report_tight(self.collapse_tight.clone())
@@ -1627,30 +1615,3 @@ mod tests {
}
}
/// The contextual underlay tab's button: framed in the accent colour.
fn underlay_tab_button<'a>(active: bool) -> Element<'a, Message> {
let btn = button(text(t!("PDF Underlay")).size(12))
.on_press(Message::RibbonSelectUnderlayTab)
.style(move |theme: &Theme, status| {
let palette = theme.palette();
let pair = match (active, status) {
(true, _) => palette.background.weakest,
(false, button::Status::Hovered) => palette.background.weak,
_ => palette.background.base,
};
button::Style {
background: (active || matches!(status, button::Status::Hovered))
.then_some(Background::Color(pair.color)),
text_color: if active { pair.text } else { palette.primary.base.color },
border: Border {
color: palette.primary.base.color,
width: if active { 2.0 } else { 1.0 },
radius: 0.0.into(),
},
shadow: iced::Shadow::default(),
snap: false,
}
})
.padding([5, 14]);
container(btn).into()
}
-230
View File
@@ -1,230 +0,0 @@
//! The contextual "PDF Underlay" tab: shown and brought forward while only
//! PDF underlays are selected, gone when the selection changes.
use iced::widget::{column, row, slider, text, text_input};
use iced::{Element, Length};
use super::*;
use crate::modules::{ModuleEvent, ToolDef};
/// What the tab shows for the selected underlay (the first one).
#[derive(Clone, Debug, PartialEq)]
pub struct UnderlayContext {
pub contrast: u8,
pub fade: u8,
pub monochrome: bool,
pub shown: bool,
pub snap: bool,
pub contrast_text: String,
pub fade_text: String,
}
impl UnderlayContext {
pub fn new(contrast: u8, fade: u8, monochrome: bool, shown: bool, snap: bool) -> Self {
Self {
contrast,
fade,
monochrome,
shown,
snap,
contrast_text: contrast.to_string(),
fade_text: fade.to_string(),
}
}
}
/// The tab's edits; the host writes them to the selected underlays.
#[derive(Clone, Debug)]
pub enum UnderlayTabMsg {
Contrast(u8),
Fade(u8),
ContrastText(String),
FadeText(String),
/// Slider released or value submitted: write contrast and fade.
Commit,
}
pub(super) const ADJUST: &str = "Adjust";
fn tool(id: &'static str, label: &'static str, icon: &'static [u8]) -> ToolDef {
ToolDef {
id,
label,
icon: IconKind::Svg(icon),
event: ModuleEvent::Command(id.to_string()),
}
}
fn groups() -> &'static [RibbonGroup] {
static GROUPS: std::sync::OnceLock<Vec<RibbonGroup>> = std::sync::OnceLock::new();
const FRAMES: &[u8] = include_bytes!("../../../assets/icons/underlay_frames.svg");
const CLIP: &[u8] = include_bytes!("../../../assets/icons/xclip.svg");
GROUPS.get_or_init(|| {
vec![
RibbonGroup {
title: ADJUST,
tools: vec![RibbonItem::LargeTool(tool(
"_PDFULMONO",
"Display in\nMonochrome",
FRAMES,
))],
},
RibbonGroup {
title: "Clipping",
tools: vec![
RibbonItem::LargeTool(tool("_PDFULCLIP", "Create Clipping\nBoundary", CLIP)),
RibbonItem::LargeTool(tool("_PDFULUNCLIP", "Remove\nClipping", CLIP)),
],
},
RibbonGroup {
title: "Options",
tools: vec![
RibbonItem::LargeTool(tool("_PDFULSHOW", "Show\nUnderlay", FRAMES)),
RibbonItem::LargeTool(tool(
"_PDFULSNAP",
"Enable\nSnap",
include_bytes!("../../../assets/icons/snap_underlays.svg"),
)),
RibbonItem::LargeTool(tool(
"EXTERNALREFERENCES",
"External\nReferences",
crate::ui::icons::FOLDER_OPEN,
)),
],
},
RibbonGroup {
title: "PDF Layers",
tools: vec![RibbonItem::LargeTool(tool(
"ULAYERS",
"Edit\nLayers",
include_bytes!("../../../assets/icons/underlay_layers.svg"),
))],
},
RibbonGroup {
title: "PDF Import",
tools: vec![RibbonItem::LargeTool(tool(
"_PDFULIMPORT",
"Import As\nObjects",
include_bytes!("../../../assets/icons/cui_import.svg"),
))],
},
]
})
}
/// Contrast / Fade: a label, a 0–100 slider and the value box.
fn adjust_rows<'a>(ctx: &'a UnderlayContext) -> Element<'a, Message> {
let line = |label: &'static str,
value: u8,
buffer: &'a str,
on_slide: fn(u8) -> UnderlayTabMsg,
on_text: fn(String) -> UnderlayTabMsg| {
row![
text(t!(label)).size(11).width(Length::Fixed(64.0)),
slider(0..=100u8, value, move |v| Message::UnderlayTab(on_slide(v)))
.on_release(Message::UnderlayTab(UnderlayTabMsg::Commit))
.width(Length::Fixed(110.0)),
text_input("", buffer)
.on_input(move |s| Message::UnderlayTab(on_text(s)))
.on_submit(Message::UnderlayTab(UnderlayTabMsg::Commit))
.size(11)
.padding([2, 4])
.width(Length::Fixed(44.0)),
]
.spacing(6)
.align_y(iced::Center)
};
column![
line(
"Contrast",
ctx.contrast,
&ctx.contrast_text,
UnderlayTabMsg::Contrast,
UnderlayTabMsg::ContrastText,
),
line(
"Fade",
ctx.fade,
&ctx.fade_text,
UnderlayTabMsg::Fade,
UnderlayTabMsg::FadeText,
),
]
.spacing(8)
.padding([6, 4])
.into()
}
impl Ribbon {
/// Show the tab for the selected underlay, or drop it (`None`). A tab
/// that appears comes to the front; one that goes returns the tab that
/// was active before it.
pub fn set_underlay_context(&mut self, context: Option<UnderlayContext>) {
match (&self.underlay_ctx, &context) {
(None, Some(_)) => self.underlay_tab_active = true,
(Some(_), None) => self.underlay_tab_active = false,
_ => {}
}
// A value being typed survives a refresh that leaves the value as is.
let mut context = context;
if let (Some(old), Some(new)) = (&self.underlay_ctx, &mut context) {
if (old.contrast, old.fade) == (new.contrast, new.fade) {
new.contrast_text = old.contrast_text.clone();
new.fade_text = old.fade_text.clone();
}
}
self.underlay_ctx = context;
}
pub fn underlay_context_mut(&mut self) -> Option<&mut UnderlayContext> {
self.underlay_ctx.as_mut()
}
/// Select a tab by its module id; "pdf_underlay" is the contextual tab.
pub fn select_by_id(&mut self, id: &str) -> bool {
if id == "pdf_underlay" {
self.select_underlay_tab();
return self.underlay_tab_active;
}
match self.modules.iter().position(|m| m.id() == id) {
Some(index) => {
self.select(index);
true
}
None => false,
}
}
pub fn select_underlay_tab(&mut self) {
if self.underlay_ctx.is_some() {
self.underlay_tab_active = true;
}
}
pub(super) fn underlay_tab_shown(&self) -> bool {
self.underlay_tab_active && self.underlay_ctx.is_some()
}
pub(super) fn underlay_panels<'a>(
&'a self,
ts: widgets::ToggleState,
style_ctx: &StyleContext<'_>,
) -> Vec<Panel<'a>> {
let Some(ctx) = self.underlay_ctx.as_ref() else {
return Vec::new();
};
groups()
.iter()
.map(|g| {
let lead = |_: bool| -> Vec<Element<'a, Message>> {
if g.title == ADJUST {
vec![adjust_rows(ctx)]
} else {
Vec::new()
}
};
self.panel(g, ts, style_ctx, &lead)
})
.collect()
}
}
-6
View File
@@ -40,9 +40,6 @@ pub(super) struct ToggleState {
pub show_block_palette: bool,
pub show_file_tabs: bool,
pub show_layout_tabs: bool,
pub underlay_mono: bool,
pub underlay_shown: bool,
pub underlay_snap: bool,
}
// ── Layout constants (single source of truth: ROW_H from ui::mod) ─────────
@@ -378,9 +375,6 @@ pub(super) fn is_active_tool(
"UCSICON" => state.show_ucs_icon,
"PROPERTIES" => state.show_properties,
"BLOCKPALETTE" => state.show_block_palette,
"_PDFULMONO" => state.underlay_mono,
"_PDFULSHOW" => state.underlay_shown,
"_PDFULSNAP" => state.underlay_snap,
"FILETAB" => state.show_file_tabs,
"LAYOUTTAB" => state.show_layout_tabs,
id => active_tool.as_deref() == Some(id),
+23 -4
View File
@@ -17,6 +17,9 @@ const ICON_SIZE: f32 = 22.0;
const BTN_SIZE: f32 = 38.0;
/// Gap between the toolbar and the right edge of the canvas.
const EDGE_MARGIN: f32 = 8.0;
/// Room kept free at the top right for the view cube and the UCS list; the
/// toolbar is centred in the height below it.
const TOP_RESERVE: f32 = 200.0;
fn icon_el(icon: IconKind) -> Element<'static, Message> {
match icon {
@@ -26,6 +29,8 @@ fn icon_el(icon: IconKind) -> Element<'static, Message> {
}
fn tip_panel(label: &'static str) -> Element<'static, Message> {
// Ribbon labels break over two lines; a tooltip reads them on one.
let label = crate::t!(label).replace('\n', " ");
container(text(label).size(11))
.padding([2, 6])
.style(|theme: &Theme| {
@@ -48,12 +53,21 @@ fn tip_panel(label: &'static str) -> Element<'static, Message> {
/// centred over the canvas. Returns `None` when `tools` is empty so the caller
/// can skip pushing an overlay.
pub fn view(tools: &[ToolDef]) -> Option<Element<'static, Message>> {
view_with_active(tools, &|_| false)
}
/// As [`view`], with the tools `active` names drawn as switched on.
pub fn view_with_active(
tools: &[ToolDef],
active: &dyn Fn(&str) -> bool,
) -> Option<Element<'static, Message>> {
if tools.is_empty() {
return None;
}
let mut col = column![].spacing(4).align_x(iced::Center);
for t in tools {
let on = active(t.id);
let btn = button(icon_el(t.icon))
.on_press(Message::RibbonToolClick {
tool_id: t.id.to_string(),
@@ -61,18 +75,22 @@ pub fn view(tools: &[ToolDef]) -> Option<Element<'static, Message>> {
})
.width(Length::Fixed(BTN_SIZE))
.height(Length::Fixed(BTN_SIZE))
.style(|theme: &Theme, status| {
.style(move |theme: &Theme, status| {
let palette = theme.palette();
let hovered = matches!(
status,
button::Status::Hovered | button::Status::Pressed
);
button::Style {
background: hovered
.then_some(Background::Color(palette.background.strong.color)),
background: if on {
Some(Background::Color(palette.primary.weak.color))
} else {
hovered.then_some(Background::Color(palette.background.strong.color))
},
border: Border {
radius: 3.0.into(),
..Default::default()
color: palette.primary.base.color,
width: if on { 1.0 } else { 0.0 },
},
text_color: palette.background.base.text,
..Default::default()
@@ -107,6 +125,7 @@ pub fn view(tools: &[ToolDef]) -> Option<Element<'static, Message>> {
.align_y(iced::Center)
.padding(iced::Padding {
right: EDGE_MARGIN,
top: TOP_RESERVE,
..iced::Padding::ZERO
})
.into(),