feat(ui): refine ribbon and view navigation
This commit is contained in:
parent
675538eb8d
commit
d1f9055901
14 changed files with 466 additions and 116 deletions
|
|
@ -1,8 +1,65 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
<line x1="4" y1="18" x2="14" y2="18" stroke="#4cc9f0" stroke-width="1.5"/>
|
<svg
|
||||||
<polyline points="11,15 14,18 11,21" stroke="#4cc9f0" stroke-width="1.5"/>
|
viewBox="0 0 24 24"
|
||||||
<text x="15" y="21" font-size="5" fill="#4cc9f0" stroke="none">X</text>
|
width="64"
|
||||||
<line x1="4" y1="18" x2="4" y2="6" stroke="#e0e0e0" stroke-width="1.5"/>
|
height="64"
|
||||||
<polyline points="1,9 4,6 7,9" stroke="#e0e0e0" stroke-width="1.5"/>
|
fill="none"
|
||||||
<text x="1" y="5" font-size="5" fill="#e0e0e0" stroke="none">Y</text>
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
version="1.1"
|
||||||
|
id="svg2"
|
||||||
|
sodipodi:docname="ucs_icon.svg"
|
||||||
|
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs
|
||||||
|
id="defs2" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview2"
|
||||||
|
pagecolor="#505050"
|
||||||
|
bordercolor="#eeeeee"
|
||||||
|
borderopacity="1"
|
||||||
|
inkscape:showpageshadow="0"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#505050"
|
||||||
|
inkscape:zoom="10.099031"
|
||||||
|
inkscape:cx="31.488168"
|
||||||
|
inkscape:cy="33.121989"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="939"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg2" />
|
||||||
|
<line
|
||||||
|
x1="7.880435"
|
||||||
|
y1="16.5"
|
||||||
|
x2="19.619564"
|
||||||
|
y2="16.5"
|
||||||
|
stroke="#4cc9f0"
|
||||||
|
stroke-width="1.62521"
|
||||||
|
id="line1" />
|
||||||
|
<polyline
|
||||||
|
points="11,15 14,18 11,21"
|
||||||
|
stroke="#4cc9f0"
|
||||||
|
stroke-width="1.5"
|
||||||
|
id="polyline1"
|
||||||
|
transform="translate(5.7684904,-1.5665742)" />
|
||||||
|
<line
|
||||||
|
x1="7.75"
|
||||||
|
y1="16.5"
|
||||||
|
x2="7.75"
|
||||||
|
y2="4.5"
|
||||||
|
stroke="#e0e0e0"
|
||||||
|
stroke-width="1.5"
|
||||||
|
id="line2" />
|
||||||
|
<polyline
|
||||||
|
points="1,9 4,6 7,9"
|
||||||
|
stroke="#e0e0e0"
|
||||||
|
stroke-width="1.5"
|
||||||
|
id="polyline2"
|
||||||
|
transform="translate(3.75,-1.5)" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 596 B After Width: | Height: | Size: 1.6 KiB |
|
|
@ -99,6 +99,7 @@ impl OpenCADStudio {
|
||||||
self.ucs_icon_selected = false;
|
self.ucs_icon_selected = false;
|
||||||
self.ucs_icon_hover = false;
|
self.ucs_icon_hover = false;
|
||||||
self.tabs[i].pan_mode = false;
|
self.tabs[i].pan_mode = false;
|
||||||
|
self.tabs[i].orbit_mode = false;
|
||||||
let _ = self.on_viewport_exit();
|
let _ = self.on_viewport_exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,7 @@ impl OpenCADStudio {
|
||||||
pub(super) fn dispatch_inquiry(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
|
pub(super) fn dispatch_inquiry(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
|
||||||
match cmd {
|
match cmd {
|
||||||
"3DORBIT" => {
|
"3DORBIT" => {
|
||||||
self.command_line
|
self.tabs[i].orbit_mode = true;
|
||||||
.push_info(crate::t!("3D Orbit: drag with right mouse button.").as_ref());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Selection utilities ───────────────────────────────────────
|
// ── Selection utilities ───────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -76,9 +76,10 @@ impl OpenCADStudio {
|
||||||
// template-property override too (#239).
|
// template-property override too (#239).
|
||||||
self.restore_add_selected_defaults();
|
self.restore_add_selected_defaults();
|
||||||
}
|
}
|
||||||
// Starting any command leaves interactive PAN mode (the PAN arm below
|
// Starting any command leaves interactive navigation modes (their own
|
||||||
// re-enables it).
|
// command arms below re-enable the selected one).
|
||||||
self.tabs[i].pan_mode = false;
|
self.tabs[i].pan_mode = false;
|
||||||
|
self.tabs[i].orbit_mode = false;
|
||||||
// Reset the last committed point so the first click of the new command
|
// Reset the last committed point so the first click of the new command
|
||||||
// is not constrained by ortho/polar relative to a previous command's endpoint.
|
// is not constrained by ortho/polar relative to a previous command's endpoint.
|
||||||
self.last_point = None;
|
self.last_point = None;
|
||||||
|
|
|
||||||
|
|
@ -209,6 +209,9 @@ pub(super) struct DocumentTab {
|
||||||
/// device with no middle mouse button (a trackpad / web client). Exited
|
/// device with no middle mouse button (a trackpad / web client). Exited
|
||||||
/// with Esc or by starting another command.
|
/// with Esc or by starting another command.
|
||||||
pub(super) pan_mode: bool,
|
pub(super) pan_mode: bool,
|
||||||
|
/// Interactive 3-D orbit mode. While active, a left-button drag follows
|
||||||
|
/// the same camera-orbit path as Shift + middle-button drag.
|
||||||
|
pub(super) orbit_mode: bool,
|
||||||
/// Per-plugin document state (`plugin::BuiltinPlugin` manifest id → state).
|
/// Per-plugin document state (`plugin::BuiltinPlugin` manifest id → state).
|
||||||
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
|
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
|
||||||
pub(super) plugin_state: HashMap<&'static str, Box<dyn Any + Send + Sync>>,
|
pub(super) plugin_state: HashMap<&'static str, Box<dyn Any + Send + Sync>>,
|
||||||
|
|
@ -479,6 +482,7 @@ impl DocumentTab {
|
||||||
thumbnail_cache_key: None,
|
thumbnail_cache_key: None,
|
||||||
is_start: false,
|
is_start: false,
|
||||||
pan_mode: false,
|
pan_mode: false,
|
||||||
|
orbit_mode: false,
|
||||||
plugin_state: HashMap::new(),
|
plugin_state: HashMap::new(),
|
||||||
suspended_cmd: None,
|
suspended_cmd: None,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1893,8 +1893,12 @@ pub enum Message {
|
||||||
LayerStateEditorFilter(String),
|
LayerStateEditorFilter(String),
|
||||||
LayerStateEditorSave,
|
LayerStateEditorSave,
|
||||||
LayerStateEditorCancel,
|
LayerStateEditorCancel,
|
||||||
CursorMoved(Point),
|
/// ViewCube-local cursor movement, tagged with the floating viewport that
|
||||||
ViewportClick,
|
/// owned the overlay when the event was produced (`None` = Model layout).
|
||||||
|
CursorMoved(Point, Option<acadrust::Handle>),
|
||||||
|
/// ViewCube press with the same owner tag, so a stale overlay event can
|
||||||
|
/// never fall through and rotate a different camera.
|
||||||
|
ViewportClick(Option<acadrust::Handle>),
|
||||||
ViewportMove(Point),
|
ViewportMove(Point),
|
||||||
ViewportLeftPress,
|
ViewportLeftPress,
|
||||||
ViewportLeftRelease,
|
ViewportLeftRelease,
|
||||||
|
|
|
||||||
|
|
@ -569,16 +569,25 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||||
self.tabs[i].snap_result = None;
|
self.tabs[i].snap_result = None;
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
// Leave interactive PAN mode (and end any in-flight pan drag).
|
// Leave an interactive navigation mode and end its in-flight
|
||||||
if self.tabs[self.active_tab].pan_mode {
|
// drag. Orbit exits silently; PAN keeps its existing message.
|
||||||
|
if self.tabs[self.active_tab].pan_mode
|
||||||
|
|| self.tabs[self.active_tab].orbit_mode
|
||||||
|
{
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
|
let was_pan = self.tabs[i].pan_mode;
|
||||||
self.tabs[i].pan_mode = false;
|
self.tabs[i].pan_mode = false;
|
||||||
|
self.tabs[i].orbit_mode = false;
|
||||||
{
|
{
|
||||||
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
||||||
sel.middle_down = false;
|
sel.middle_down = false;
|
||||||
sel.middle_last_pos = None;
|
sel.middle_last_pos = None;
|
||||||
|
sel.orbit_pivot = None;
|
||||||
}
|
}
|
||||||
self.command_line.push_output(crate::t!("PAN ended.").as_ref());
|
if was_pan {
|
||||||
|
self.command_line.push_output(crate::t!("PAN ended.").as_ref());
|
||||||
|
}
|
||||||
|
self.ribbon.deactivate_tool();
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
// Grip popup intercepts Escape — dismisses the menu
|
// Grip popup intercepts Escape — dismisses the menu
|
||||||
|
|
|
||||||
|
|
@ -138,7 +138,11 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
||||||
// dialog owners keep theirs; the command end / modal
|
// dialog owners keep theirs; the command end / modal
|
||||||
// close clears those. (#355)
|
// close clears those. (#355)
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
if self.tabs[i].active_cmd.is_none() && self.active_modal.is_none() {
|
if self.tabs[i].active_cmd.is_none()
|
||||||
|
&& self.active_modal.is_none()
|
||||||
|
&& !self.tabs[i].pan_mode
|
||||||
|
&& !self.tabs[i].orbit_mode
|
||||||
|
{
|
||||||
self.ribbon.deactivate_tool();
|
self.ribbon.deactivate_tool();
|
||||||
}
|
}
|
||||||
return task;
|
return task;
|
||||||
|
|
|
||||||
|
|
@ -2420,7 +2420,7 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Cursor / viewport messages ─────────────────────────────────
|
// ── Cursor / viewport messages ─────────────────────────────────
|
||||||
Message::CursorMoved(p) => self.on_cursor_moved(p),
|
Message::CursorMoved(p, viewport) => self.on_cursor_moved(p, viewport),
|
||||||
|
|
||||||
Message::ViewportMove(p) => self.on_viewport_move(p),
|
Message::ViewportMove(p) => self.on_viewport_move(p),
|
||||||
|
|
||||||
|
|
@ -2587,7 +2587,7 @@ impl OpenCADStudio {
|
||||||
|
|
||||||
Message::ViewportScroll(delta) => self.on_viewport_scroll(delta),
|
Message::ViewportScroll(delta) => self.on_viewport_scroll(delta),
|
||||||
|
|
||||||
Message::ViewportClick => self.on_viewport_click(),
|
Message::ViewportClick(viewport) => self.on_viewport_click(viewport),
|
||||||
|
|
||||||
Message::WindowResized(w, h) => {
|
Message::WindowResized(w, h) => {
|
||||||
self.vp_size = ((w - 440.0).max(200.0), h);
|
self.vp_size = ((w - 440.0).max(200.0), h);
|
||||||
|
|
|
||||||
|
|
@ -482,6 +482,12 @@ impl OpenCADStudio {
|
||||||
if let Some(session) = self.tabs[i].active_block_edit_session_mut() {
|
if let Some(session) = self.tabs[i].active_block_edit_session_mut() {
|
||||||
session.editor_camera = camera;
|
session.editor_camera = camera;
|
||||||
}
|
}
|
||||||
|
} else if self.tabs[i].scene.active_viewport.is_some() {
|
||||||
|
// Floating-viewport navigation writes its camera straight to
|
||||||
|
// the viewport entity. Syncing the separate main camera here
|
||||||
|
// can overwrite the saved Model/Paper view with a camera that
|
||||||
|
// does not own this change.
|
||||||
|
self.tabs[i].dirty = true;
|
||||||
} else if self.tabs[i].scene.sync_camera_to_document() {
|
} else if self.tabs[i].scene.sync_camera_to_document() {
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
}
|
}
|
||||||
|
|
@ -541,7 +547,11 @@ impl OpenCADStudio {
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn on_cursor_moved(&mut self, p: Point) -> Task<Message> {
|
pub(super) fn on_cursor_moved(
|
||||||
|
&mut self,
|
||||||
|
p: Point,
|
||||||
|
expected_viewport: Option<acadrust::Handle>,
|
||||||
|
) -> Task<Message> {
|
||||||
if self.color_pick_target.is_some() {
|
if self.color_pick_target.is_some() {
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
|
|
@ -552,6 +562,12 @@ impl OpenCADStudio {
|
||||||
// the full canvas in model space, or of the active
|
// the full canvas in model space, or of the active
|
||||||
// viewport's screen rectangle in a paper layout.
|
// viewport's screen rectangle in a paper layout.
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
|
if self.tabs[i].scene.active_viewport != expected_viewport
|
||||||
|
|| (expected_viewport.is_none()
|
||||||
|
&& self.tabs[i].scene.current_layout != "Model")
|
||||||
|
{
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
let (vw, vh) = self.tabs[i].scene.selection.borrow().vp_size;
|
let (vw, vh) = self.tabs[i].scene.selection.borrow().vp_size;
|
||||||
let (ox, oy) = match self.tabs[i]
|
let (ox, oy) = match self.tabs[i]
|
||||||
.scene
|
.scene
|
||||||
|
|
@ -716,7 +732,7 @@ impl OpenCADStudio {
|
||||||
// — the requested Zoom=wheel / Pan=MMB / Rotate=Shift+MMB
|
// — the requested Zoom=wheel / Pan=MMB / Rotate=Shift+MMB
|
||||||
// scheme (#229). Floating viewports and paper keep the
|
// scheme (#229). Floating viewports and paper keep the
|
||||||
// plain MMB pan.
|
// plain MMB pan.
|
||||||
if self.shift_down {
|
if self.shift_down || self.tabs[i].orbit_mode {
|
||||||
if self.tabs[i].scene.active_viewport.is_some() {
|
if self.tabs[i].scene.active_viewport.is_some() {
|
||||||
// Orbit the floating viewport's own model view.
|
// Orbit the floating viewport's own model view.
|
||||||
drop(sel);
|
drop(sel);
|
||||||
|
|
@ -746,8 +762,12 @@ impl OpenCADStudio {
|
||||||
self.tabs[i].scene.selection.borrow_mut().middle_last_pos = Some(p);
|
self.tabs[i].scene.selection.borrow_mut().middle_last_pos = Some(p);
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
// Paper sheet is top-locked: Shift+MMB falls through
|
// Paper sheet is top-locked. Shift+MMB keeps its existing
|
||||||
// to the plain pan below.
|
// pan fallback; the explicit orbit tool does nothing here.
|
||||||
|
if self.tabs[i].orbit_mode {
|
||||||
|
sel.middle_last_pos = Some(p);
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Pan scale uses the active tile's size (ortho size
|
// Pan scale uses the active tile's size (ortho size
|
||||||
// is relative to viewport height), so a tiled pane
|
// is relative to viewport height), so a tiled pane
|
||||||
|
|
@ -2185,10 +2205,9 @@ impl OpenCADStudio {
|
||||||
};
|
};
|
||||||
let (vw, vh) = vp_size;
|
let (vw, vh) = vp_size;
|
||||||
|
|
||||||
// PAN mode: a left press begins a pan drag. Reuse the middle-
|
// Interactive navigation tools reuse the middle-button movement path,
|
||||||
// button pan path (the move handler pans whenever `middle_down`),
|
// so no selection/pick logic runs while the left button drives them.
|
||||||
// so no selection/pick logic runs while panning.
|
if self.tabs[i].orbit_mode || self.tabs[i].pan_mode {
|
||||||
if self.tabs[i].pan_mode {
|
|
||||||
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
||||||
sel.middle_down = true;
|
sel.middle_down = true;
|
||||||
sel.middle_last_pos = Some(p);
|
sel.middle_last_pos = Some(p);
|
||||||
|
|
@ -2357,12 +2376,13 @@ impl OpenCADStudio {
|
||||||
pub(super) fn on_viewport_left_release(&mut self) -> Task<Message> {
|
pub(super) fn on_viewport_left_release(&mut self) -> Task<Message> {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
|
|
||||||
// PAN mode: end the pan drag but stay in pan mode for the next
|
// Navigation mode: end this drag but keep the tool armed for the next
|
||||||
// drag (exit is Esc / another command). Mirror of the press.
|
// left drag (exit is Esc / another command). Mirror of the press.
|
||||||
if self.tabs[i].pan_mode {
|
if self.tabs[i].orbit_mode || self.tabs[i].pan_mode {
|
||||||
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
||||||
sel.middle_down = false;
|
sel.middle_down = false;
|
||||||
sel.middle_last_pos = None;
|
sel.middle_last_pos = None;
|
||||||
|
sel.orbit_pivot = None;
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3886,8 +3906,17 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn on_viewport_click(&mut self) -> Task<Message> {
|
pub(super) fn on_viewport_click(
|
||||||
|
&mut self,
|
||||||
|
expected_viewport: Option<acadrust::Handle>,
|
||||||
|
) -> Task<Message> {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
|
if self.tabs[i].scene.active_viewport != expected_viewport
|
||||||
|
|| (expected_viewport.is_none()
|
||||||
|
&& self.tabs[i].scene.current_layout != "Model")
|
||||||
|
{
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
let rot = self.tabs[i].scene.active_view_rotation_mat();
|
let rot = self.tabs[i].scene.active_view_rotation_mat();
|
||||||
let (vw, vh) = self.tabs[i].scene.selection.borrow().vp_size;
|
let (vw, vh) = self.tabs[i].scene.selection.borrow().vp_size;
|
||||||
// The ViewCube draws in the top-right of whichever area
|
// The ViewCube draws in the top-right of whichever area
|
||||||
|
|
@ -3929,17 +3958,17 @@ impl OpenCADStudio {
|
||||||
} else {
|
} else {
|
||||||
scene::CubeRegion::Corner(id)
|
scene::CubeRegion::Corner(id)
|
||||||
};
|
};
|
||||||
return Task::done(Message::ViewCubeSnap(region));
|
return self.on_view_cube_snap(region);
|
||||||
}
|
}
|
||||||
if let Some(region) = scene::hit_test(cx, cy, w, h, rot, VIEWCUBE_PX) {
|
if let Some(region) = scene::hit_test(cx, cy, w, h, rot, VIEWCUBE_PX) {
|
||||||
return Task::done(Message::ViewCubeSnap(region));
|
return self.on_view_cube_snap(region);
|
||||||
}
|
}
|
||||||
// Compass cardinals are world-fixed: hit-test through the camera-
|
// Compass cardinals are world-fixed: hit-test through the camera-
|
||||||
// only rotation (strip the UCS) so the target matches the drawn
|
// only rotation (strip the UCS) so the target matches the drawn
|
||||||
// N/E/S/W, and snap in world frame.
|
// N/E/S/W, and snap in world frame.
|
||||||
let rot_world = rot * self.tabs[i].scene.viewcube_ucs_mat().inverse();
|
let rot_world = rot * self.tabs[i].scene.viewcube_ucs_mat().inverse();
|
||||||
if let Some(card) = scene::hit_test_cardinal(cx, cy, w, h, rot_world, VIEWCUBE_PX) {
|
if let Some(card) = scene::hit_test_cardinal(cx, cy, w, h, rot_world, VIEWCUBE_PX) {
|
||||||
return Task::done(Message::ViewCubeSnapWorld(card.face_region()));
|
return self.on_view_cube_snap_world(card.face_region());
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -671,7 +671,7 @@ impl OpenCADStudio {
|
||||||
dividers,
|
dividers,
|
||||||
pane_move_rect,
|
pane_move_rect,
|
||||||
pane_drop_rect,
|
pane_drop_rect,
|
||||||
tab.pan_mode,
|
tab.pan_mode || tab.orbit_mode,
|
||||||
self.ribbon.open_dropdown.is_some(),
|
self.ribbon.open_dropdown.is_some(),
|
||||||
hover_locked,
|
hover_locked,
|
||||||
crosshair_background(tab, is_paper),
|
crosshair_background(tab, is_paper),
|
||||||
|
|
@ -886,15 +886,18 @@ impl OpenCADStudio {
|
||||||
// both layered ABOVE the viewport mouse_area so they receive
|
// both layered ABOVE the viewport mouse_area so they receive
|
||||||
// clicks (the shader viewport sits below it). Positioned with
|
// clicks (the shader viewport sits below it). Positioned with
|
||||||
// leading Spaces sized to the viewport's screen rectangle.
|
// leading Spaces sized to the viewport's screen rectangle.
|
||||||
let active_vp_rect: Option<iced::Rectangle> = if is_paper && !tab.is_start {
|
let active_vp_rect: Option<(acadrust::Handle, iced::Rectangle)> =
|
||||||
tab.scene.active_viewport.and_then(|h| {
|
if is_paper && !tab.is_start {
|
||||||
let (cw, ch) = tab.scene.selection.borrow().vp_size;
|
tab.scene.active_viewport.and_then(|h| {
|
||||||
tab.scene.viewport_screen_rect(h, (cw, ch))
|
let (cw, ch) = tab.scene.selection.borrow().vp_size;
|
||||||
})
|
tab.scene
|
||||||
} else {
|
.viewport_screen_rect(h, (cw, ch))
|
||||||
None
|
.map(|rect| (h, rect))
|
||||||
};
|
})
|
||||||
if let Some(rect) = active_vp_rect {
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
if let Some((active_vp, rect)) = active_vp_rect {
|
||||||
// Clip the outline to the visible canvas. Clamping only the origin
|
// Clip the outline to the visible canvas. Clamping only the origin
|
||||||
// (max(0.0)) while keeping the full width/height shifted the whole
|
// (max(0.0)) while keeping the full width/height shifted the whole
|
||||||
// outline inward when the viewport ran off the top/left edge, so
|
// outline inward when the viewport ran off the top/left edge, so
|
||||||
|
|
@ -964,7 +967,7 @@ impl OpenCADStudio {
|
||||||
let cube_x = (rect.x + rect.width - VIEWCUBE_HIT_SIZE - VIEWCUBE_PAD).max(0.0);
|
let cube_x = (rect.x + rect.width - VIEWCUBE_HIT_SIZE - VIEWCUBE_PAD).max(0.0);
|
||||||
let cube_y = (rect.y + VIEWCUBE_PAD).max(0.0);
|
let cube_y = (rect.y + VIEWCUBE_PAD).max(0.0);
|
||||||
|
|
||||||
let controls = iced::widget::pin(viewcube_nav_controls())
|
let controls = iced::widget::pin(viewcube_nav_controls(Some(active_vp)))
|
||||||
.position(iced::Point::new(cube_x, cube_y));
|
.position(iced::Point::new(cube_x, cube_y));
|
||||||
viewport_stack = viewport_stack.push(controls);
|
viewport_stack = viewport_stack.push(controls);
|
||||||
|
|
||||||
|
|
@ -1003,7 +1006,7 @@ impl OpenCADStudio {
|
||||||
let cube_y = (rect.y + VIEWCUBE_PAD).max(0.0);
|
let cube_y = (rect.y + VIEWCUBE_PAD).max(0.0);
|
||||||
|
|
||||||
// Cube hit area + nav controls (home / roll / nudge) as one layer.
|
// Cube hit area + nav controls (home / roll / nudge) as one layer.
|
||||||
let controls = iced::widget::pin(viewcube_nav_controls())
|
let controls = iced::widget::pin(viewcube_nav_controls(None))
|
||||||
.position(iced::Point::new(cube_x, cube_y));
|
.position(iced::Point::new(cube_x, cube_y));
|
||||||
viewport_stack = viewport_stack.push(controls);
|
viewport_stack = viewport_stack.push(controls);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,9 @@ fn vc_btn<'a>(content: Element<'a, Message>, size: f32, msg: Message) -> Element
|
||||||
|
|
||||||
/// Overlay of home / roll / nudge controls sized to the whole nav region, so
|
/// Overlay of home / roll / nudge controls sized to the whole nav region, so
|
||||||
/// the caller can position it exactly like the cube hit area.
|
/// the caller can position it exactly like the cube hit area.
|
||||||
pub(super) fn viewcube_nav_controls<'a>() -> Element<'a, Message> {
|
pub(super) fn viewcube_nav_controls<'a>(
|
||||||
|
viewport: Option<acadrust::Handle>,
|
||||||
|
) -> Element<'a, Message> {
|
||||||
use crate::scene::NudgeDir;
|
use crate::scene::NudgeDir;
|
||||||
use crate::ui::icons;
|
use crate::ui::icons;
|
||||||
let r = VIEWCUBE_REGION_PX;
|
let r = VIEWCUBE_REGION_PX;
|
||||||
|
|
@ -81,8 +83,8 @@ pub(super) fn viewcube_nav_controls<'a>() -> Element<'a, Message> {
|
||||||
.width(iced::Length::Fixed(r))
|
.width(iced::Length::Fixed(r))
|
||||||
.height(iced::Length::Fixed(r)),
|
.height(iced::Length::Fixed(r)),
|
||||||
)
|
)
|
||||||
.on_move(Message::CursorMoved)
|
.on_move(move |point| Message::CursorMoved(point, viewport))
|
||||||
.on_press(Message::ViewportClick);
|
.on_press(Message::ViewportClick(viewport));
|
||||||
|
|
||||||
let controls = stack![
|
let controls = stack![
|
||||||
cube_hit,
|
cube_hit,
|
||||||
|
|
|
||||||
|
|
@ -1398,6 +1398,7 @@ fn collapse_button<'a>(
|
||||||
compact: bool,
|
compact: bool,
|
||||||
) -> Element<'a, Message> {
|
) -> Element<'a, Message> {
|
||||||
let title = group.title;
|
let title = group.title;
|
||||||
|
let localized_title = t!(title).into_owned();
|
||||||
|
|
||||||
// Tightest form: one button = the panel's FIRST tool icon + its title + ▾.
|
// Tightest form: one button = the panel's FIRST tool icon + its title + ▾.
|
||||||
// Clicking opens the flyout listing every tool; no tool runs directly at this
|
// Clicking opens the flyout listing every tool; no tool runs directly at this
|
||||||
|
|
@ -1407,23 +1408,31 @@ fn collapse_button<'a>(
|
||||||
Some(ik) => make_icon(ik, SMALL_ICON),
|
Some(ik) => make_icon(ik, SMALL_ICON),
|
||||||
None => text("").into(),
|
None => text("").into(),
|
||||||
};
|
};
|
||||||
return button(
|
let content = button(
|
||||||
column![
|
column![
|
||||||
icon,
|
icon,
|
||||||
row![
|
row![
|
||||||
text(t!(title)).size(9).style(muted_text_style),
|
text(localized_title.clone())
|
||||||
|
.size(9)
|
||||||
|
.width(Fill)
|
||||||
|
.align_x(iced::Center)
|
||||||
|
.wrapping(iced::advanced::text::Wrapping::WordOrGlyph)
|
||||||
|
.style(muted_text_style),
|
||||||
crate::ui::icons::themed_secondary_arrow_down(8.0),
|
crate::ui::icons::themed_secondary_arrow_down(8.0),
|
||||||
]
|
]
|
||||||
.spacing(3)
|
.spacing(3)
|
||||||
|
.width(Fill)
|
||||||
.align_y(iced::Center),
|
.align_y(iced::Center),
|
||||||
]
|
]
|
||||||
.align_x(iced::Center)
|
.align_x(iced::Center)
|
||||||
.spacing(2),
|
.spacing(2)
|
||||||
|
.width(Fill),
|
||||||
)
|
)
|
||||||
.on_press(Message::ToggleRibbonPanel(title.to_string()))
|
.on_press(Message::ToggleRibbonPanel(title.to_string()))
|
||||||
.style(button::subtle)
|
.style(button::subtle)
|
||||||
.padding([3, 5])
|
.width(Fill)
|
||||||
.into();
|
.padding([3, 5]);
|
||||||
|
return automatic_large_button(localized_title, content.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collapsed (not yet tight): a large representative-tool face — a live button
|
// Collapsed (not yet tight): a large representative-tool face — a live button
|
||||||
|
|
@ -1464,15 +1473,23 @@ fn collapse_button<'a>(
|
||||||
|
|
||||||
let opener = button(
|
let opener = button(
|
||||||
row![
|
row![
|
||||||
text(title.to_string()).size(9).style(muted_text_style),
|
text(localized_title.clone())
|
||||||
|
.size(9)
|
||||||
|
.width(Fill)
|
||||||
|
.align_x(iced::Center)
|
||||||
|
.wrapping(iced::advanced::text::Wrapping::WordOrGlyph)
|
||||||
|
.style(muted_text_style),
|
||||||
crate::ui::icons::themed_secondary_arrow_down(8.0),
|
crate::ui::icons::themed_secondary_arrow_down(8.0),
|
||||||
]
|
]
|
||||||
.spacing(3)
|
.spacing(3)
|
||||||
|
.width(Fill)
|
||||||
.align_y(iced::Center),
|
.align_y(iced::Center),
|
||||||
)
|
)
|
||||||
.on_press(Message::ToggleRibbonPanel(title.to_string()))
|
.on_press(Message::ToggleRibbonPanel(title.to_string()))
|
||||||
.style(button::subtle)
|
.style(button::subtle)
|
||||||
|
.width(Fill)
|
||||||
.padding([1, 4]);
|
.padding([1, 4]);
|
||||||
|
let opener = automatic_large_button(localized_title, opener.into());
|
||||||
|
|
||||||
// The large face fills a fixed slot so a collapsed panel is shorter than a full
|
// The large face fills a fixed slot so a collapsed panel is shorter than a full
|
||||||
// 3-row panel, letting `CollapsePanels` shrink the ribbon row.
|
// 3-row panel, letting `CollapsePanels` shrink the ribbon row.
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,21 @@
|
||||||
// free functions used by the Ribbon view/overlay methods.
|
// free functions used by the Ribbon view/overlay methods.
|
||||||
|
|
||||||
use rustc_hash::FxHashMap as HashMap;
|
use rustc_hash::FxHashMap as HashMap;
|
||||||
|
use std::cell::RefCell;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use acadrust::types::{Color as AcadColor, LineWeight};
|
use acadrust::types::{Color as AcadColor, LineWeight};
|
||||||
|
use iced::advanced::{
|
||||||
|
layout, mouse, overlay, renderer, text as advanced_text, widget, Layout, Shell, Widget,
|
||||||
|
};
|
||||||
// Ribbon tooltips anchor to the right of their button so the cursor — which
|
// Ribbon tooltips anchor to the right of their button so the cursor — which
|
||||||
// rests on the button itself — never covers the tip text. (#143)
|
// rests on the button itself — never covers the tip text. (#143)
|
||||||
use iced::widget::tooltip::Position as TipPos;
|
use iced::widget::tooltip::Position as TipPos;
|
||||||
use iced::widget::{button, column, container, row, text, tooltip};
|
use iced::widget::{button, column, container, row, text, tooltip};
|
||||||
use iced::{Background, Border, Color, Element, Fill, Length, Padding, Theme};
|
use iced::{
|
||||||
|
Background, Border, Color, Element, Event, Fill, Length, Padding, Pixels, Rectangle, Size,
|
||||||
|
Theme, Vector,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::app::Message;
|
use crate::app::Message;
|
||||||
use crate::modules::{IconKind, ModuleEvent, RibbonItem, StyleKey, ToolDef};
|
use crate::modules::{IconKind, ModuleEvent, RibbonItem, StyleKey, ToolDef};
|
||||||
|
|
@ -48,6 +55,11 @@ pub(super) const LARGE_ICON: f32 = ROW_H * 1.5;
|
||||||
pub(super) const SMALL_ICON: f32 = ROW_H * 0.7;
|
pub(super) const SMALL_ICON: f32 = ROW_H * 0.7;
|
||||||
/// Width of a 3-row (large) button.
|
/// Width of a 3-row (large) button.
|
||||||
pub(super) const LARGE_W: f32 = ROW_H * 2.2;
|
pub(super) const LARGE_W: f32 = ROW_H * 2.2;
|
||||||
|
/// Horizontal button padding surrounding a large tool's label.
|
||||||
|
const LARGE_LABEL_HPAD: f32 = 8.0;
|
||||||
|
/// Large ribbon labels use at most two lines before their button grows.
|
||||||
|
const LARGE_LABEL_LINES: f32 = 2.0;
|
||||||
|
const LARGE_LABEL_SIZE: f32 = 10.0;
|
||||||
/// Width of a 1-row (small) button.
|
/// Width of a 1-row (small) button.
|
||||||
pub(super) const SMALL_W: f32 = ROW_H;
|
pub(super) const SMALL_W: f32 = ROW_H;
|
||||||
/// Width of the ▾ strip on a small dropdown.
|
/// Width of the ▾ strip on a small dropdown.
|
||||||
|
|
@ -61,6 +73,215 @@ pub(super) const TOOL_BAR_H: f32 = 3.0 * ROW_H + 18.0;
|
||||||
/// shorter than a full 3-row panel — the ribbon height follows it down.
|
/// shorter than a full 3-row panel — the ribbon height follows it down.
|
||||||
pub(super) const COLLAPSED_FACE_H: f32 = LARGE_ICON + 20.0;
|
pub(super) const COLLAPSED_FACE_H: f32 = LARGE_ICON + 20.0;
|
||||||
|
|
||||||
|
// ── Automatic large-button sizing ────────────────────────────────────────
|
||||||
|
|
||||||
|
thread_local! {
|
||||||
|
/// Ribbon layout runs on every pointer-driven view update. Cache the font-
|
||||||
|
/// measured width per translated label so the automatic sizing stays cheap.
|
||||||
|
static LARGE_WIDTH_CACHE: RefCell<HashMap<String, f32>> =
|
||||||
|
RefCell::new(HashMap::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ribbon_label_bounds(
|
||||||
|
renderer: &iced::Renderer,
|
||||||
|
label: &str,
|
||||||
|
width: f32,
|
||||||
|
wrapping: advanced_text::Wrapping,
|
||||||
|
) -> Size {
|
||||||
|
use advanced_text::{Paragraph as _, Renderer as _};
|
||||||
|
|
||||||
|
let paragraph = <iced::Renderer as advanced_text::Renderer>::Paragraph::with_text(
|
||||||
|
advanced_text::Text {
|
||||||
|
content: label,
|
||||||
|
bounds: Size::new(width, f32::INFINITY),
|
||||||
|
size: Pixels(LARGE_LABEL_SIZE),
|
||||||
|
line_height: advanced_text::LineHeight::default(),
|
||||||
|
font: renderer.default_font(),
|
||||||
|
align_x: advanced_text::Alignment::Center,
|
||||||
|
align_y: iced::alignment::Vertical::Center,
|
||||||
|
shaping: advanced_text::Shaping::default(),
|
||||||
|
wrapping,
|
||||||
|
ellipsis: advanced_text::Ellipsis::None,
|
||||||
|
hint_factor: None,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
paragraph.min_bounds()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Measure the translated label at the normal button width. It wraps first;
|
||||||
|
/// only labels that would need more than two lines widen their button. The
|
||||||
|
/// binary search uses the renderer's real font metrics, so locale and UI scale
|
||||||
|
/// changes do not rely on character-count estimates.
|
||||||
|
fn measure_large_width(renderer: &iced::Renderer, label: &str) -> f32 {
|
||||||
|
let base_inner = (LARGE_W - LARGE_LABEL_HPAD).max(1.0);
|
||||||
|
let line_height = advanced_text::LineHeight::default()
|
||||||
|
.to_absolute(Pixels(LARGE_LABEL_SIZE))
|
||||||
|
.0;
|
||||||
|
let max_label_height = line_height * LARGE_LABEL_LINES + 0.5;
|
||||||
|
let fits = |width: f32| {
|
||||||
|
ribbon_label_bounds(
|
||||||
|
renderer,
|
||||||
|
label,
|
||||||
|
width,
|
||||||
|
advanced_text::Wrapping::WordOrGlyph,
|
||||||
|
)
|
||||||
|
.height
|
||||||
|
<= max_label_height
|
||||||
|
};
|
||||||
|
|
||||||
|
if fits(base_inner) {
|
||||||
|
return LARGE_W;
|
||||||
|
}
|
||||||
|
|
||||||
|
let natural = ribbon_label_bounds(
|
||||||
|
renderer,
|
||||||
|
label,
|
||||||
|
f32::INFINITY,
|
||||||
|
advanced_text::Wrapping::None,
|
||||||
|
)
|
||||||
|
.width
|
||||||
|
.max(base_inner);
|
||||||
|
if !fits(natural) {
|
||||||
|
return (natural + LARGE_LABEL_HPAD).ceil();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut low = base_inner;
|
||||||
|
let mut high = natural;
|
||||||
|
for _ in 0..10 {
|
||||||
|
let mid = (low + high) * 0.5;
|
||||||
|
if fits(mid) {
|
||||||
|
high = mid;
|
||||||
|
} else {
|
||||||
|
low = mid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(high + LARGE_LABEL_HPAD).ceil().max(LARGE_W)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn automatic_large_width(renderer: &iced::Renderer, label: &str) -> f32 {
|
||||||
|
if let Some(width) = LARGE_WIDTH_CACHE.with(|cache| cache.borrow().get(label).copied()) {
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
|
||||||
|
let width = measure_large_width(renderer, label);
|
||||||
|
LARGE_WIDTH_CACHE.with(|cache| {
|
||||||
|
cache.borrow_mut().insert(label.to_string(), width);
|
||||||
|
});
|
||||||
|
width
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AutomaticLargeWidth<'a> {
|
||||||
|
label: String,
|
||||||
|
content: Element<'a, Message>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget<Message, Theme, iced::Renderer> for AutomaticLargeWidth<'_> {
|
||||||
|
fn tag(&self) -> widget::tree::Tag {
|
||||||
|
self.content.as_widget().tag()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> widget::tree::State {
|
||||||
|
self.content.as_widget().state()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diff(&mut self, tree: &mut widget::Tree) {
|
||||||
|
self.content.as_widget_mut().diff(tree);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self) -> Size<Length> {
|
||||||
|
Size::new(Length::Shrink, self.content.as_widget().size().height)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn layout(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut widget::Tree,
|
||||||
|
renderer: &iced::Renderer,
|
||||||
|
limits: &layout::Limits,
|
||||||
|
) -> layout::Node {
|
||||||
|
let width = automatic_large_width(renderer, &self.label);
|
||||||
|
self.content.as_widget_mut().layout(
|
||||||
|
tree,
|
||||||
|
renderer,
|
||||||
|
&limits.width(Length::Fixed(width)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn operate(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut widget::Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
renderer: &iced::Renderer,
|
||||||
|
operation: &mut dyn widget::Operation,
|
||||||
|
) {
|
||||||
|
self.content
|
||||||
|
.as_widget_mut()
|
||||||
|
.operate(tree, layout, renderer, operation);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
&mut self,
|
||||||
|
tree: &mut widget::Tree,
|
||||||
|
event: &Event,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
renderer: &iced::Renderer,
|
||||||
|
shell: &mut Shell<'_, Message>,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
self.content.as_widget_mut().update(
|
||||||
|
tree, event, layout, cursor, renderer, shell, viewport,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_interaction(
|
||||||
|
&self,
|
||||||
|
tree: &widget::Tree,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
renderer: &iced::Renderer,
|
||||||
|
) -> mouse::Interaction {
|
||||||
|
self.content
|
||||||
|
.as_widget()
|
||||||
|
.mouse_interaction(tree, layout, cursor, viewport, renderer)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(
|
||||||
|
&self,
|
||||||
|
tree: &widget::Tree,
|
||||||
|
renderer: &mut iced::Renderer,
|
||||||
|
theme: &Theme,
|
||||||
|
style: &renderer::Style,
|
||||||
|
layout: Layout<'_>,
|
||||||
|
cursor: mouse::Cursor,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
) {
|
||||||
|
self.content
|
||||||
|
.as_widget()
|
||||||
|
.draw(tree, renderer, theme, style, layout, cursor, viewport);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn overlay<'b>(
|
||||||
|
&'b mut self,
|
||||||
|
tree: &'b mut widget::Tree,
|
||||||
|
layout: Layout<'b>,
|
||||||
|
renderer: &iced::Renderer,
|
||||||
|
viewport: &Rectangle,
|
||||||
|
translation: Vector,
|
||||||
|
) -> Option<overlay::Element<'b, Message, Theme, iced::Renderer>> {
|
||||||
|
self.content
|
||||||
|
.as_widget_mut()
|
||||||
|
.overlay(tree, layout, renderer, viewport, translation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn automatic_large_button<'a>(
|
||||||
|
label: String,
|
||||||
|
content: Element<'a, Message>,
|
||||||
|
) -> Element<'a, Message> {
|
||||||
|
Element::new(AutomaticLargeWidth { label, content })
|
||||||
|
}
|
||||||
|
|
||||||
// ── Tab-bar constants ──────────────────────────────────────────────────────
|
// ── Tab-bar constants ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
pub(super) const TOP_ARR_W: f32 = 12.0;
|
pub(super) const TOP_ARR_W: f32 = 12.0;
|
||||||
|
|
@ -459,30 +680,40 @@ pub(super) fn render_large_dropdown<'a>(
|
||||||
.and_then(|cmd| items.iter().find(|(c, _, _)| *c == cmd).map(|(_, lbl, _)| *lbl))
|
.and_then(|cmd| items.iter().find(|(c, _, _)| *c == cmd).map(|(_, lbl, _)| *lbl))
|
||||||
.or_else(|| items.first().map(|(_, lbl, _)| *lbl))
|
.or_else(|| items.first().map(|(_, lbl, _)| *lbl))
|
||||||
.unwrap_or(id);
|
.unwrap_or(id);
|
||||||
let label = explicit_label.unwrap_or(cur_label);
|
let label = t!(explicit_label.unwrap_or(cur_label)).into_owned();
|
||||||
let tip_text = format!("{}\n{} {}", t!(cur_label), t!("Command:"), last);
|
let tip_text = format!("{}\n{} {}", t!(cur_label), t!("Command:"), last);
|
||||||
let arr_tip = format!("{} {}", t!(label), t!("options"));
|
let arr_tip = format!("{} {}", label, t!("options"));
|
||||||
|
|
||||||
// Icon on top with the label beneath it, then the ▾ strip at the very bottom.
|
// The label owns the bottom of the face. The icon's Fill container centers
|
||||||
|
// it in all remaining space between the button top and the label.
|
||||||
let top_btn = button(
|
let top_btn = button(
|
||||||
column![
|
column![
|
||||||
make_icon_dim(cur_icon, LARGE_ICON, dim),
|
container(make_icon_dim(cur_icon, LARGE_ICON, dim))
|
||||||
text(t!(label))
|
.width(Fill)
|
||||||
|
.height(Fill)
|
||||||
|
.align_x(iced::Center)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
text(label.clone())
|
||||||
.size(10)
|
.size(10)
|
||||||
|
.width(Fill)
|
||||||
|
.align_x(iced::Center)
|
||||||
|
.wrapping(advanced_text::Wrapping::WordOrGlyph)
|
||||||
.style(move |theme: &Theme| tool_label_style(theme, dim)),
|
.style(move |theme: &Theme| tool_label_style(theme, dim)),
|
||||||
]
|
]
|
||||||
.align_x(iced::Center)
|
.align_x(iced::Center)
|
||||||
.spacing(3),
|
.spacing(0)
|
||||||
|
.width(Fill)
|
||||||
|
.height(Fill),
|
||||||
)
|
)
|
||||||
.on_press(Message::RibbonToolClick {
|
.on_press(Message::RibbonToolClick {
|
||||||
tool_id: last.to_string(),
|
tool_id: last.to_string(),
|
||||||
event: ModuleEvent::Command(last.to_string()),
|
event: ModuleEvent::Command(last.to_string()),
|
||||||
})
|
})
|
||||||
.style(move |theme: &Theme, status| tool_btn_style(theme, active, status))
|
.style(move |theme: &Theme, status| tool_btn_style(theme, active, status))
|
||||||
.width(Length::Fixed(LARGE_W))
|
.width(Fill)
|
||||||
.height(Fill)
|
.height(Fill)
|
||||||
.padding(Padding {
|
.padding(Padding {
|
||||||
top: 6.0,
|
top: 4.0,
|
||||||
right: 4.0,
|
right: 4.0,
|
||||||
bottom: 2.0,
|
bottom: 2.0,
|
||||||
left: 4.0,
|
left: 4.0,
|
||||||
|
|
@ -499,7 +730,7 @@ pub(super) fn render_large_dropdown<'a>(
|
||||||
.style(move |theme: &Theme, status| {
|
.style(move |theme: &Theme, status| {
|
||||||
tool_btn_style(theme, dd_open, status)
|
tool_btn_style(theme, dd_open, status)
|
||||||
})
|
})
|
||||||
.width(Length::Fixed(LARGE_W))
|
.width(Fill)
|
||||||
.height(LARGE_ARR)
|
.height(LARGE_ARR)
|
||||||
.padding(0);
|
.padding(0);
|
||||||
|
|
||||||
|
|
@ -512,13 +743,12 @@ pub(super) fn render_large_dropdown<'a>(
|
||||||
.delay(Duration::from_millis(400))
|
.delay(Duration::from_millis(400))
|
||||||
.style(tip_style);
|
.style(tip_style);
|
||||||
|
|
||||||
PosReport::new(
|
let content = column![top_with_tip, arr_with_tip]
|
||||||
id,
|
.spacing(0)
|
||||||
column![top_with_tip, arr_with_tip]
|
.width(Fill)
|
||||||
.spacing(0)
|
.height(Fill);
|
||||||
.width(Length::Fixed(LARGE_W))
|
|
||||||
.height(Fill),
|
PosReport::new(id, automatic_large_button(label, content.into()))
|
||||||
)
|
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -546,28 +776,42 @@ pub(super) fn render_large<'a>(
|
||||||
let dim = start_dimmed(&state, &t.event);
|
let dim = start_dimmed(&state, &t.event);
|
||||||
let event = t.event.clone();
|
let event = t.event.clone();
|
||||||
let tool_id = t.id.to_string();
|
let tool_id = t.id.to_string();
|
||||||
let tip_text = format!("{}\n{} {}", t!(t.label), t!("Command:"), t.id);
|
let label = t!(t.label).into_owned();
|
||||||
|
let tip_text = format!("{}\n{} {}", label, t!("Command:"), t.id);
|
||||||
let btn = button(
|
let btn = button(
|
||||||
column![
|
column![
|
||||||
make_icon_dim(t.icon, LARGE_ICON, dim),
|
container(make_icon_dim(t.icon, LARGE_ICON, dim))
|
||||||
text(t!(t.label))
|
.width(Fill)
|
||||||
|
.height(Fill)
|
||||||
|
.align_x(iced::Center)
|
||||||
|
.align_y(iced::Center),
|
||||||
|
text(label.clone())
|
||||||
.size(10)
|
.size(10)
|
||||||
|
.width(Fill)
|
||||||
|
.align_x(iced::Center)
|
||||||
|
.wrapping(advanced_text::Wrapping::WordOrGlyph)
|
||||||
.style(move |theme: &Theme| tool_label_style(theme, dim)),
|
.style(move |theme: &Theme| tool_label_style(theme, dim)),
|
||||||
]
|
]
|
||||||
.align_x(iced::Center)
|
.align_x(iced::Center)
|
||||||
.spacing(3),
|
.spacing(0)
|
||||||
|
.width(Fill)
|
||||||
|
.height(Fill),
|
||||||
)
|
)
|
||||||
.on_press(Message::RibbonToolClick { tool_id, event })
|
.on_press(Message::RibbonToolClick { tool_id, event })
|
||||||
.style(move |theme: &Theme, status| tool_btn_style(theme, active, status))
|
.style(move |theme: &Theme, status| tool_btn_style(theme, active, status))
|
||||||
.width(Length::Fixed(LARGE_W))
|
.width(Fill)
|
||||||
.height(Fill)
|
.height(Fill)
|
||||||
.padding(Padding {
|
.padding(Padding {
|
||||||
top: 6.0,
|
top: 4.0,
|
||||||
right: 4.0,
|
right: 4.0,
|
||||||
bottom: 4.0,
|
bottom: 4.0,
|
||||||
left: 4.0,
|
left: 4.0,
|
||||||
});
|
});
|
||||||
tooltip(btn, make_tip(tip_text), TipPos::Right)
|
tooltip(
|
||||||
|
automatic_large_button(label, btn.into()),
|
||||||
|
make_tip(tip_text),
|
||||||
|
TipPos::Right,
|
||||||
|
)
|
||||||
.gap(6.0)
|
.gap(6.0)
|
||||||
.delay(Duration::from_millis(400))
|
.delay(Duration::from_millis(400))
|
||||||
.style(tip_style)
|
.style(tip_style)
|
||||||
|
|
@ -749,44 +993,20 @@ pub(super) fn render_large<'a>(
|
||||||
state,
|
state,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
let mp_active = is_active_tool(match_prop.id, active_tool, &state);
|
render_large(
|
||||||
let mp_dim = start_dimmed(&state, &match_prop.event);
|
&RibbonItem::LargeTool(match_prop.clone()),
|
||||||
let mp_event = match_prop.event.clone();
|
active_tool,
|
||||||
let mp_id = match_prop.id.to_string();
|
open_dd,
|
||||||
let mp_tip = format!(
|
last_cmd,
|
||||||
"{}\n{} {}",
|
state,
|
||||||
t!(match_prop.label),
|
layer_infos,
|
||||||
t!("Command:"),
|
active_layer,
|
||||||
match_prop.id
|
active_color,
|
||||||
);
|
active_linetype,
|
||||||
let mp_btn = button(
|
active_lineweight,
|
||||||
column![
|
style_ctx,
|
||||||
make_icon_dim(match_prop.icon, LARGE_ICON, mp_dim),
|
false,
|
||||||
text(t!(match_prop.label))
|
|
||||||
.size(10)
|
|
||||||
.style(move |theme: &Theme| tool_label_style(theme, mp_dim)),
|
|
||||||
]
|
|
||||||
.align_x(iced::Center)
|
|
||||||
.spacing(3),
|
|
||||||
)
|
)
|
||||||
.on_press(Message::RibbonToolClick {
|
|
||||||
tool_id: mp_id,
|
|
||||||
event: mp_event,
|
|
||||||
})
|
|
||||||
.style(move |theme: &Theme, status| tool_btn_style(theme, mp_active, status))
|
|
||||||
.width(Length::Fixed(LARGE_W))
|
|
||||||
.height(Fill)
|
|
||||||
.padding(Padding {
|
|
||||||
top: 6.0,
|
|
||||||
right: 4.0,
|
|
||||||
bottom: 4.0,
|
|
||||||
left: 4.0,
|
|
||||||
});
|
|
||||||
tooltip(mp_btn, make_tip(mp_tip), TipPos::Right)
|
|
||||||
.gap(6.0)
|
|
||||||
.delay(Duration::from_millis(400))
|
|
||||||
.style(tip_style)
|
|
||||||
.into()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const PROP_W: f32 = 130.0;
|
const PROP_W: f32 = 130.0;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue