feat(ui): MTEXT editor in shared modal + resizable dialogs

Route the MTEXT editor through the shared in-canvas modal frame with an
Apply-at-top toolbar (style-manager style): Apply commits and keeps the
editor open (creates on first apply, updates in place after), the ✕
cancels. Give every modal a bottom-right corner resize grip driven by a
shared grow-only `modal_resize` delta that resets with `modal_offset` on
open/close, so each dialog opens at its natural size and can be dragged
larger — style managers included.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-07-19 15:47:09 +03:00
commit 977701b47e
10 changed files with 221 additions and 70 deletions

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 9 L9 21 M21 15 L15 21 M21 21 L20 21"/></svg>

After

Width:  |  Height:  |  Size: 213 B

View file

@ -448,6 +448,12 @@ pub(super) struct OpenCADStudio {
modal_drag_last: Option<Point>, modal_drag_last: Option<Point>,
/// True while the modal title bar is held (a drag is in progress). /// True while the modal title bar is held (a drag is in progress).
modal_dragging: bool, modal_dragging: bool,
/// How far the user has dragged the modal's corner resize grip from the
/// dialog's natural size (added to each modal's default width/height). Reset
/// with `modal_offset` so every dialog opens at its own size.
modal_resize: iced::Vector,
/// True while the modal's corner resize grip is held.
modal_resizing: bool,
// ── Attribute editor dialog (ATTEDIT / double-click a block) ─────────── // ── Attribute editor dialog (ATTEDIT / double-click a block) ───────────
/// INSERT whose attributes the editor modal is editing (`None` = closed). /// INSERT whose attributes the editor modal is editing (`None` = closed).
attr_editor_handle: Option<acadrust::Handle>, attr_editor_handle: Option<acadrust::Handle>,
@ -1845,6 +1851,10 @@ pub enum Message {
MTextCaretBlink, MTextCaretBlink,
/// Commit the editor: create or update the MText entity. /// Commit the editor: create or update the MText entity.
MTextOk, MTextOk,
/// Apply the buffer to the entity but leave the editor open (the button).
MTextApply,
/// Grab the resizable modal's corner grip (a drag resizes it).
ModalResizeGrab,
/// Discard the editor without creating / changing the entity. /// Discard the editor without creating / changing the entity.
MTextCancel, MTextCancel,
// ── In-place single-line TEXT editor ──────────────────────────────── // ── In-place single-line TEXT editor ────────────────────────────────
@ -2227,6 +2237,8 @@ impl OpenCADStudio {
modal_offset: iced::Vector::ZERO, modal_offset: iced::Vector::ZERO,
modal_drag_last: None, modal_drag_last: None,
modal_dragging: false, modal_dragging: false,
modal_resize: iced::Vector::ZERO,
modal_resizing: false,
attr_editor_handle: None, attr_editor_handle: None,
attr_editor_block: String::new(), attr_editor_block: String::new(),
attr_editor_rows: Vec::new(), attr_editor_rows: Vec::new(),

View file

@ -559,6 +559,11 @@ impl super::OpenCADStudio {
} }
} }
self.mtext_editor = Some(state); self.mtext_editor = Some(state);
// Open centred at the editor's natural size: it renders through the
// shared modal frame, which `modal_offset` positions and `modal_resize`
// grows (both draggable, so reset on open).
self.modal_offset = iced::Vector::ZERO;
self.modal_resize = iced::Vector::ZERO;
self.rebuild_mtext_preview(); self.rebuild_mtext_preview();
// Place the caret at the end so typing works without a click first. // Place the caret at the end so typing works without a click first.
let end = self.mtext_vis_count(); let end = self.mtext_vis_count();
@ -1084,6 +1089,59 @@ impl super::OpenCADStudio {
true true
} }
/// Apply the buffer to the drawing but keep the editor open — the
/// style-manager model, where **Apply** commits and the dialog stays up
/// (closing is the ✕). A brand-new MText is created on the first apply and
/// then updated in place on later applies, so repeated Apply never leaves
/// duplicate entities behind.
pub(super) fn mtext_apply(&mut self) {
let i = self.active_tab;
let (mut mt, editing, body_empty) = match self.mtext_editor.as_ref() {
Some(ed) => (
ed.build_mtext(),
ed.editing,
ed.content.text().trim().is_empty(),
),
None => return,
};
if body_empty {
return;
}
if let Some(h) = editing {
self.push_undo_snapshot(i, "MTEXT");
match self.tabs[i].scene.document.get_entity_mut(h) {
Some(EntityType::MText(t)) => {
t.value = mt.value;
t.height = mt.height;
t.attachment_point = mt.attachment_point;
t.line_spacing_factor = mt.line_spacing_factor;
t.rectangle_width = mt.rectangle_width;
}
Some(EntityType::MultiLeader(ml)) => {
ml.context.text_string = mt.value;
ml.context.text_height = mt.height;
ml.context.line_spacing_factor = mt.line_spacing_factor;
if mt.rectangle_width > 0.0 {
ml.context.text_width = mt.rectangle_width;
}
}
_ => {}
}
self.tabs[i].scene.bump_geometry();
self.tabs[i].dirty = true;
} else {
mt.rotation = self.tabs[i].ucs_rotation_angle();
self.push_undo_snapshot(i, "MTEXT");
let handle = self.commit_entity_handle(EntityType::MText(mt));
self.tabs[i].dirty = true;
// Bind the editor to the fresh entity so the next Apply updates it.
if let (Some(h), Some(ed)) = (handle, self.mtext_editor.as_mut()) {
ed.editing = Some(h);
}
}
self.refresh_properties();
}
/// Discard the editor without changing the drawing. /// Discard the editor without changing the drawing.
pub(super) fn mtext_cancel(&mut self) { pub(super) fn mtext_cancel(&mut self) {
self.mtext_editor = None; self.mtext_editor = None;

View file

@ -702,6 +702,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
self.layer_delete_pending = Some((names, count)); self.layer_delete_pending = Some((names, count));
self.active_modal = Some(crate::app::ModalKind::LayerDeleteWarning); self.active_modal = Some(crate::app::ModalKind::LayerDeleteWarning);
self.modal_offset = iced::Vector::ZERO; self.modal_offset = iced::Vector::ZERO;
self.modal_resize = iced::Vector::ZERO;
} }
Task::none() Task::none()
} }
@ -712,6 +713,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
let i = self.active_tab; let i = self.active_tab;
self.active_modal = None; self.active_modal = None;
self.modal_offset = iced::Vector::ZERO; self.modal_offset = iced::Vector::ZERO;
self.modal_resize = iced::Vector::ZERO;
let Some((names, _)) = self.layer_delete_pending.take() else { let Some((names, _)) = self.layer_delete_pending.take() else {
return Task::none(); return Task::none();
}; };
@ -1668,6 +1670,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
self.attr_editor_handle = Some(handle); self.attr_editor_handle = Some(handle);
self.active_modal = Some(crate::app::ModalKind::AttributeEditor); self.active_modal = Some(crate::app::ModalKind::AttributeEditor);
self.modal_offset = iced::Vector::ZERO; self.modal_offset = iced::Vector::ZERO;
self.modal_resize = iced::Vector::ZERO;
} }
Err(msg) => self.command_line.push_error(msg), Err(msg) => self.command_line.push_error(msg),
} }
@ -1687,6 +1690,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
if self.active_modal == Some(crate::app::ModalKind::AttributeEditor) { if self.active_modal == Some(crate::app::ModalKind::AttributeEditor) {
self.active_modal = None; self.active_modal = None;
self.modal_offset = iced::Vector::ZERO; self.modal_offset = iced::Vector::ZERO;
self.modal_resize = iced::Vector::ZERO;
} }
self.attr_editor_handle = None; self.attr_editor_handle = None;
self.attr_editor_block.clear(); self.attr_editor_block.clear();

View file

@ -130,8 +130,9 @@ impl OpenCADStudio {
_ => {} _ => {}
} }
self.active_modal = None; self.active_modal = None;
// Recentre the next dialog and drop any in-progress drag. // Recentre / reset the size of the next dialog and drop any drag.
self.modal_offset = iced::Vector::ZERO; self.modal_offset = iced::Vector::ZERO;
self.modal_resize = iced::Vector::ZERO;
self.modal_drag_last = None; self.modal_drag_last = None;
self.modal_dragging = false; self.modal_dragging = false;
} }
@ -2269,6 +2270,10 @@ impl OpenCADStudio {
let committed = self.mtext_commit(); let committed = self.mtext_commit();
self.post_editor_closed(committed) self.post_editor_closed(committed)
} }
Message::MTextApply => {
self.mtext_apply();
Task::none()
}
Message::MTextCancel => { Message::MTextCancel => {
self.mtext_cancel(); self.mtext_cancel();
self.post_editor_closed(false) self.post_editor_closed(false)
@ -3200,11 +3205,33 @@ impl OpenCADStudio {
self.modal_drag_last = None; self.modal_drag_last = None;
Task::none() Task::none()
} }
Message::ModalResizeGrab => {
// Start a resize; the first ModalDragMove seeds the reference.
self.modal_resizing = true;
self.modal_drag_last = None;
Task::none()
}
Message::ModalDragMove(p) => { Message::ModalDragMove(p) => {
if self.modal_dragging { if let Some(last) = self.modal_drag_last {
if let Some(last) = self.modal_drag_last { let (dx, dy) = (p.x - last.x, p.y - last.y);
self.modal_offset.x += p.x - last.x; if self.modal_resizing {
self.modal_offset.y += p.y - last.y; // The grip sits bottom-right, so dragging out grows the
// box. The delta is added to each dialog's natural size,
// so clamp it at zero — dragging in past the natural size
// does nothing (the natural size is the floor).
let nx = (self.modal_resize.x + dx).max(0.0);
let ny = (self.modal_resize.y + dy).max(0.0);
let (rx, ry) = (nx - self.modal_resize.x, ny - self.modal_resize.y);
self.modal_resize.x = nx;
self.modal_resize.y = ny;
// The box is centred, so shift the centre by half the
// growth to pin the top-left corner — the grip then
// tracks the cursor instead of drifting at half speed.
self.modal_offset.x += rx * 0.5;
self.modal_offset.y += ry * 0.5;
} else if self.modal_dragging {
self.modal_offset.x += dx;
self.modal_offset.y += dy;
// Clamp so the dialog stops at the window edge instead // Clamp so the dialog stops at the window edge instead
// of being squeezed (the off-centre padding shrinks the // of being squeezed (the off-centre padding shrinks the
// dialog once it overlaps a border). // dialog once it overlaps a border).
@ -3217,12 +3244,15 @@ impl OpenCADStudio {
self.modal_offset.y = self.modal_offset.y.clamp(-max_y, max_y); self.modal_offset.y = self.modal_offset.y.clamp(-max_y, max_y);
} }
} }
}
if self.modal_dragging || self.modal_resizing {
self.modal_drag_last = Some(p); self.modal_drag_last = Some(p);
} }
Task::none() Task::none()
} }
Message::ModalDragRelease => { Message::ModalDragRelease => {
self.modal_dragging = false; self.modal_dragging = false;
self.modal_resizing = false;
self.modal_drag_last = None; self.modal_drag_last = None;
Task::none() Task::none()
} }

View file

@ -1221,7 +1221,13 @@ impl OpenCADStudio {
.iter() .iter()
.map(|s| s.name.clone()) .map(|s| s.name.clone())
.collect(); .collect();
viewport_stack = viewport_stack.push(mtext_editor_overlay(ed, styles, canvas)); viewport_stack = viewport_stack.push(mtext_editor_overlay(
ed,
styles,
canvas,
self.modal_offset,
self.modal_resize,
));
} }
if let Some(ed) = &self.text_inline { if let Some(ed) = &self.text_inline {
viewport_stack = viewport_stack.push(text_inline_overlay(ed, canvas)); viewport_stack = viewport_stack.push(text_inline_overlay(ed, canvas));
@ -1536,7 +1542,13 @@ impl OpenCADStudio {
// the native (single main window) and web builds. // the native (single main window) and web builds.
let base: Element<'_, Message> = match self.modal_content() { let base: Element<'_, Message> = match self.modal_content() {
Some(content) => { Some(content) => {
crate::ui::modal::modal(composed, content, Message::CloseModal, self.modal_offset) crate::ui::modal::modal(
composed,
content,
Message::CloseModal,
self.modal_offset,
true,
)
} }
None => composed.into(), None => composed.into(),
}; };
@ -1552,6 +1564,7 @@ impl OpenCADStudio {
.height(iced::Length::Fixed(470.0)), .height(iced::Length::Fixed(470.0)),
Message::CloseColorPicker, Message::CloseColorPicker,
iced::Vector::ZERO, iced::Vector::ZERO,
false,
) )
} else { } else {
base base
@ -1596,7 +1609,12 @@ impl OpenCADStudio {
ScaleManager => (520, 360), ScaleManager => (520, 360),
AnnoObjectScale => (360, 420), AnnoObjectScale => (360, 420),
}; };
Some((w as f32 + EXTRA_W, h as f32 + EXTRA_H)) // Include the user's corner-resize growth so the drag clamp tracks the
// dialog's actual footprint.
Some((
w as f32 + EXTRA_W + self.modal_resize.x,
h as f32 + EXTRA_H + self.modal_resize.y,
))
} }
} }

View file

@ -6,13 +6,17 @@ impl OpenCADStudio {
/// Build the currently-open modal dialog's content (Plan B), or `None`. /// Build the currently-open modal dialog's content (Plan B), or `None`.
/// Each former pop-up window is constructed here and given a bounded size /// Each former pop-up window is constructed here and given a bounded size
/// (About shrinks to its content). Rendered as an overlay by `view_main`. /// (About shrinks to its content). Rendered as an overlay by `view_main`.
pub(super) fn modal_content(&self) -> Option<Element<'_, Message>> { pub(super) fn modal_content<'s>(&'s self) -> Option<Element<'s, Message>> {
fn sized<'a>(e: Element<'a, Message>, w: u16, h: u16) -> Element<'a, Message> { // Grow every dialog by the shared corner-resize delta (grow-only, so the
// natural size is the floor). The delta resets to zero whenever a modal
// opens/closes, so each dialog starts at its own size.
let ex = self.modal_resize;
let sized = |e: Element<'s, Message>, w: u16, h: u16| -> Element<'s, Message> {
iced::widget::container(e) iced::widget::container(e)
.width(iced::Length::Fixed(w as f32)) .width(iced::Length::Fixed(w as f32 + ex.x))
.height(iced::Length::Fixed(h as f32)) .height(iced::Length::Fixed(h as f32 + ex.y))
.into() .into()
} };
Some(match self.active_modal? { Some(match self.active_modal? {
super::super::ModalKind::About => crate::ui::window::about::view_window(), super::super::ModalKind::About => crate::ui::window::about::view_window(),
super::super::ModalKind::Shortcuts => { super::super::ModalKind::Shortcuts => {

View file

@ -349,16 +349,12 @@ pub(super) fn mtext_editor_overlay<'a>(
ed: &'a super::super::mtext_editor::MTextEditorState, ed: &'a super::super::mtext_editor::MTextEditorState,
styles: Vec<String>, styles: Vec<String>,
canvas_size: (f32, f32), canvas_size: (f32, f32),
modal_offset: iced::Vector,
modal_resize: iced::Vector,
) -> Element<'a, Message> { ) -> Element<'a, Message> {
use super::super::mtext_editor::{JustifyChoice, MTextFmt, ParaAlign}; use super::super::mtext_editor::{JustifyChoice, MTextFmt, ParaAlign};
use iced::widget::{canvas, svg}; use iced::widget::{canvas, svg};
const PANEL_BG: Color = Color {
r: 0.16,
g: 0.16,
b: 0.16,
a: 0.98,
};
const BORDER: Color = Color { const BORDER: Color = Color {
r: 0.40, r: 0.40,
g: 0.40, g: 0.40,
@ -548,22 +544,13 @@ pub(super) fn mtext_editor_overlay<'a>(
.on_press(Message::MTextLineSpacing(2.0)) .on_press(Message::MTextLineSpacing(2.0))
.padding(3) .padding(3)
.style(btn_style), .style(btn_style),
iced::widget::Space::new().width(Fill),
icon_btn(
include_bytes!("../../../assets/icons/mt_ok.svg"),
Message::MTextOk
),
icon_btn(
include_bytes!("../../../assets/icons/mt_cancel.svg"),
Message::MTextCancel
),
] ]
.spacing(4) .spacing(4)
.align_y(iced::Alignment::Center); .align_y(iced::Alignment::Center);
// ── Segmented Edit | Preview toggle (between toolbar and body) ──────── // ── Body: the rendered preview (the editor is preview-only). It fills the
// ── Body: the rendered preview (the editor is preview-only) ───────── // space left by the toolbars, so the resizable modal's extra height flows
const VIEW_H: f32 = 150.0; // into the text area. ─────────────────────────────────────────────────
let body: Element<'a, Message> = { let body: Element<'a, Message> = {
let segments = mtext_preview_segments(ed); let segments = mtext_preview_segments(ed);
let (mut minx, mut miny, mut maxx, mut maxy) = (f32::MAX, f32::MAX, f32::MIN, f32::MIN); let (mut minx, mut miny, mut maxx, mut maxy) = (f32::MAX, f32::MAX, f32::MIN, f32::MIN);
@ -606,7 +593,9 @@ pub(super) fn mtext_editor_overlay<'a>(
let cv = canvas(prog) let cv = canvas(prog)
.width(Fill) .width(Fill)
.height(iced::Length::Fixed(content_h)); .height(iced::Length::Fixed(content_h));
container(iced::widget::scrollable(cv).height(iced::Length::Fixed(VIEW_H))) // The preview fills the space the toolbars leave, so the resizable
// modal's extra height goes to the text area (it scrolls if taller).
container(iced::widget::scrollable(cv).height(Fill))
.style(move |_: &Theme| container::Style { .style(move |_: &Theme| container::Style {
background: Some(Background::Color(FIELD_BG)), background: Some(Background::Color(FIELD_BG)),
border: Border { border: Border {
@ -618,33 +607,51 @@ pub(super) fn mtext_editor_overlay<'a>(
}) })
.padding(2) .padding(2)
.width(Fill) .width(Fill)
.height(Fill)
.into() .into()
}; };
let panel = container(column![row1, row2, body].spacing(5)) // ── Top action bar: Apply on the right, exactly like the style managers'
.style(move |_: &Theme| container::Style { // toolbar strip. Closing (the modal ✕) without applying discards the
background: Some(Background::Color(PANEL_BG)), // buffer, so there is no separate Cancel.
border: Border { let action_bar = container(
color: BORDER, row![
width: 1.0, iced::widget::Space::new().width(Fill),
radius: 5.0.into(), crate::ui::style::style_manager::tb_button("Apply", Message::MTextApply, true),
}, ]
..Default::default() .align_y(iced::Alignment::Center),
}) )
.padding(6) .style(|_: &Theme| container::Style {
.width(iced::Length::Fixed(640.0)); background: Some(Background::Color(crate::ui::style::style_manager::TB)),
..Default::default()
})
.width(Fill)
.padding([5, 8]);
// Keep the whole panel on-screen: clamp the anchor so it never spills past // The shared modal frame supplies the panel background, drag title bar,
// the right/bottom edge (where its toolbar buttons would be unclickable). // ✕ (which also cancels) and the resize grip. The content is a Fill column
// Width is fixed; height is the toolbar rows + the fixed VIEW_H body. // wrapped here in a fixed box (natural 660×480, grown by the shared resize
const PANEL_W: f32 = 640.0 + 14.0; // fixed width + padding/border // delta) so the modal frame shrinks to it and the corner grip can drag it.
const PANEL_H: f32 = VIEW_H + 150.0; // body + toolbars/toggle/padding let content = container(
let (cw, ch) = canvas_size; column![action_bar, row1, row2, body]
let anchor = iced::Point::new( .spacing(6)
(ed.screen_anchor.x - 10.0).clamp(0.0, (cw - PANEL_W).max(0.0)), .width(Fill)
(ed.screen_anchor.y - 90.0).clamp(0.0, (ch - PANEL_H).max(0.0)), .height(Fill),
); )
position_canvas_overlay(anchor, panel.into()) .width(iced::Length::Fixed(660.0 + modal_resize.x))
.height(iced::Length::Fixed(480.0 + modal_resize.y));
let _ = canvas_size; // positioned & sized by the modal frame now
// `modal` sizes its stack from the base layer, so the base must fill the
// area (a zero-size base collapses the whole overlay to nothing). The
// transparent fill lets the dimmed viewport show through beneath.
crate::ui::modal::modal(
iced::widget::Space::new().width(Fill).height(Fill),
content,
Message::MTextCancel,
modal_offset,
true,
)
} }
// ── Viewport right-click context menu ────────────────────────────────────── // ── Viewport right-click context menu ──────────────────────────────────────

View file

@ -53,6 +53,7 @@ pub const TRASH: &[u8] = include_bytes!("../../assets/icons/ui/trash.svg");
pub const COPY: &[u8] = include_bytes!("../../assets/icons/ui/copy.svg"); pub const COPY: &[u8] = include_bytes!("../../assets/icons/ui/copy.svg");
pub const MENU: &[u8] = include_bytes!("../../assets/icons/ui/menu.svg"); pub const MENU: &[u8] = include_bytes!("../../assets/icons/ui/menu.svg");
pub const MOVE: &[u8] = include_bytes!("../../assets/icons/ui/move.svg"); pub const MOVE: &[u8] = include_bytes!("../../assets/icons/ui/move.svg");
pub const RESIZE: &[u8] = include_bytes!("../../assets/icons/ui/resize.svg");
pub const SPLIT_V: &[u8] = include_bytes!("../../assets/icons/ui/split_v.svg"); pub const SPLIT_V: &[u8] = include_bytes!("../../assets/icons/ui/split_v.svg");
pub const SPLIT_H: &[u8] = include_bytes!("../../assets/icons/ui/split_h.svg"); pub const SPLIT_H: &[u8] = include_bytes!("../../assets/icons/ui/split_h.svg");
pub const GRID: &[u8] = include_bytes!("../../assets/icons/ui/grid.svg"); pub const GRID: &[u8] = include_bytes!("../../assets/icons/ui/grid.svg");

View file

@ -48,6 +48,7 @@ pub fn modal<'a>(
content: impl Into<Element<'a, Message>>, content: impl Into<Element<'a, Message>>,
on_close: Message, on_close: Message,
offset: Vector, offset: Vector,
resizable: bool,
) -> Element<'a, Message> { ) -> Element<'a, Message> {
let close = button(crate::ui::icons::tinted( let close = button(crate::ui::icons::tinted(
crate::ui::icons::CLOSE, crate::ui::icons::CLOSE,
@ -84,23 +85,38 @@ pub fn modal<'a>(
let title_bar = row![grip, close].spacing(6).align_y(iced::Center); let title_bar = row![grip, close].spacing(6).align_y(iced::Center);
// The column shrinks to the content's width; the title bar sits top-right let panel_style = |_: &Theme| container::Style {
// above it, mirroring the former ✕ placement with a drag handle. background: Some(Background::Color(PANEL)),
let framed = container( border: Border {
color: BORDER_C,
width: 1.0,
radius: 6.0.into(),
},
..Default::default()
};
// The dialog is always sized to its content (the caller fixes the content's
// width/height, growing it by the shared resize delta). When `resizable`, a
// corner grip is appended bottom-right; dragging it drives `ModalResizeGrab`
// + the shared drag move/release, which bumps that delta. The title bar sits
// top-right above the content either way (`align_x(Right)`).
let body = if resizable {
let resize = mouse_area(
container(crate::ui::icons::tinted(crate::ui::icons::RESIZE, 15.0, GRIP_C))
.padding([0, 2]),
)
.on_press(Message::ModalResizeGrab)
.interaction(iced::mouse::Interaction::Grab);
column![title_bar, content.into(), resize]
} else {
column![title_bar, content.into()] column![title_bar, content.into()]
.spacing(6) };
.align_x(iced::alignment::Horizontal::Right), let framed: Element<'a, Message> = container(
body.spacing(6).align_x(iced::alignment::Horizontal::Right),
) )
.padding(10) .padding(10)
.style(|_: &Theme| container::Style { .style(panel_style)
background: Some(Background::Color(PANEL)), .into();
border: Border {
color: BORDER_C,
width: 1.0,
radius: 6.0.into(),
},
..Default::default()
});
// Position via asymmetric padding (padding is non-negative): shifting a // Position via asymmetric padding (padding is non-negative): shifting a
// centred box by `d` on an axis needs (near − far) padding = 2·d there. // centred box by `d` on an axis needs (near − far) padding = 2·d there.