feat(grips): add dynamic input and precision snapping
This commit is contained in:
parent
60f5bca0d4
commit
86d7226df6
6 changed files with 218 additions and 46 deletions
|
|
@ -151,6 +151,7 @@ impl OpenCADStudio {
|
||||||
self.tabs[i].dirty = dirty_before;
|
self.tabs[i].dirty = dirty_before;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.grip_snap_wires.clear();
|
||||||
self.grip_text_verts.clear();
|
self.grip_text_verts.clear();
|
||||||
self.grip_text_slide = false;
|
self.grip_text_slide = false;
|
||||||
self.tabs[i].scene.clear_preview_wire();
|
self.tabs[i].scene.clear_preview_wire();
|
||||||
|
|
|
||||||
|
|
@ -545,6 +545,13 @@ pub(super) struct OpenCADStudio {
|
||||||
grip_originals: Vec<(acadrust::Handle, acadrust::EntityType)>,
|
grip_originals: Vec<(acadrust::Handle, acadrust::EntityType)>,
|
||||||
/// Document dirty state before the live grip mutation began.
|
/// Document dirty state before the live grip mutation began.
|
||||||
grip_dirty_before: Option<bool>,
|
grip_dirty_before: Option<bool>,
|
||||||
|
/// Frozen wire geometry of the entities being grip-edited.
|
||||||
|
///
|
||||||
|
/// The live entities are hidden from the resident hit-test set while their
|
||||||
|
/// grip preview is active. Keep their pre-drag geometry here so OSNAP,
|
||||||
|
/// Extension and OTRACK can still reference the entity's own vertices and
|
||||||
|
/// segments without snapping against the geometry being deformed.
|
||||||
|
grip_snap_wires: Vec<crate::scene::model::wire_model::WireModel>,
|
||||||
/// Drag-start snapshot of the dragged entity's SDF glyph quads. A whole-
|
/// Drag-start snapshot of the dragged entity's SDF glyph quads. A whole-
|
||||||
/// entity text move slides these each frame (translating the already-shaped
|
/// entity text move slides these each frame (translating the already-shaped
|
||||||
/// glyphs) instead of re-tessellating the run every cursor move (issue #316).
|
/// glyphs) instead of re-tessellating the run every cursor move (issue #316).
|
||||||
|
|
@ -3109,6 +3116,7 @@ impl OpenCADStudio {
|
||||||
hover_dwell: None,
|
hover_dwell: None,
|
||||||
grip_originals: Vec::new(),
|
grip_originals: Vec::new(),
|
||||||
grip_dirty_before: None,
|
grip_dirty_before: None,
|
||||||
|
grip_snap_wires: Vec::new(),
|
||||||
grip_text_verts: Vec::new(),
|
grip_text_verts: Vec::new(),
|
||||||
grip_text_slide: false,
|
grip_text_slide: false,
|
||||||
qselect: None,
|
qselect: None,
|
||||||
|
|
|
||||||
|
|
@ -282,47 +282,95 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Direct-distance entry while a normal grip stretch is active.
|
// Numeric entry while a normal grip stretch is active.
|
||||||
//
|
//
|
||||||
// The live grip-drag path has already resolved OSNAP / OTRACK /
|
// With Dynamic Input enabled, typed values live in the shared
|
||||||
// Extension / Polar / Ortho into `last_cursor_world`. A typed scalar
|
// Distance / Angle fields. Resolve those fields into an exact
|
||||||
// therefore means: move from the grip's original position by that
|
// world point. Without DYN input, preserve the existing direct-
|
||||||
// distance along the currently indicated direction.
|
// distance command-line behaviour.
|
||||||
{
|
{
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
|
|
||||||
if let Some(grip) = self.tabs[i].active_grip.clone() {
|
if let Some(grip) = self.tabs[i].active_grip.clone() {
|
||||||
if grip.mode == GripEditMode::Stretch {
|
if grip.mode == GripEditMode::Stretch {
|
||||||
let text =
|
let dyn_locked = self.tabs[i]
|
||||||
crate::app::expr_eval::eval_to_string(self.command_line.input.trim());
|
.dyn_fields
|
||||||
|
.iter()
|
||||||
|
.any(|field| field.buffer.is_some());
|
||||||
|
|
||||||
if let Some(dist) = crate::app::expr_eval::eval_number(text.trim()) {
|
let target = if dyn_locked {
|
||||||
let cursor = self.tabs[i].last_cursor_world;
|
// Distance only:
|
||||||
|
// keep the cursor's current direction.
|
||||||
|
//
|
||||||
|
// Distance + Angle:
|
||||||
|
// resolve both typed values from the grip's
|
||||||
|
// original position (`dyn_anchor`).
|
||||||
|
self.dyn_resolve_point()
|
||||||
|
} else {
|
||||||
|
// Legacy command-line direct-distance entry.
|
||||||
|
let text = crate::app::expr_eval::eval_to_string(
|
||||||
|
self.command_line.input.trim(),
|
||||||
|
);
|
||||||
|
|
||||||
// If the cursor is following OTRACK or Extension, use the actual
|
crate::app::expr_eval::eval_number(text.trim()).map(|dist| {
|
||||||
// reference ray that is currently driving the cursor. Otherwise
|
let cursor = self.tabs[i].last_cursor_world;
|
||||||
// fall back to the direction from the original grip position,
|
|
||||||
// which covers Polar / Ortho / free direct-distance entry.
|
|
||||||
let target = if let Some((base, dir)) = self.active_distance_ray(i) {
|
|
||||||
base + dir * dist
|
|
||||||
} else {
|
|
||||||
let direction = cursor - grip.origin_world;
|
|
||||||
let len = direction.length();
|
|
||||||
|
|
||||||
if len <= 1e-12 {
|
// If the cursor is following OTRACK or Extension,
|
||||||
self.command_line.push_error(
|
// preserve the existing reference-ray behaviour.
|
||||||
"Move the cursor in a direction before entering a distance.",
|
if let Some((base, dir)) = self.active_distance_ray(i) {
|
||||||
);
|
base + dir * dist
|
||||||
return self.focus_cmd_input();
|
} else {
|
||||||
|
let direction = cursor - grip.origin_world;
|
||||||
|
let len = direction.length();
|
||||||
|
|
||||||
|
if len <= 1e-12 {
|
||||||
|
grip.origin_world
|
||||||
|
} else {
|
||||||
|
grip.origin_world + direction / len * dist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(target) = target {
|
||||||
|
// Typed Dynamic Input can commit a grip before the mouse has moved.
|
||||||
|
//
|
||||||
|
// Normally the first ViewportMove initializes the grip preview handles and
|
||||||
|
// snapshots the original entities. Without that move the document changes,
|
||||||
|
// so the refreshed grips move, but the resident wire tessellation is never
|
||||||
|
// invalidated by the normal grip-finalization path.
|
||||||
|
//
|
||||||
|
// Seed the same bookkeeping here before modifying the entities.
|
||||||
|
if self.grip_preview_handles.is_empty() {
|
||||||
|
let mut seen_handles = rustc_hash::FxHashSet::default();
|
||||||
|
|
||||||
|
let edited_handles: Vec<_> = grip
|
||||||
|
.targets
|
||||||
|
.iter()
|
||||||
|
.map(|target| target.handle)
|
||||||
|
.filter(|handle| seen_handles.insert(*handle))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if self.grip_dirty_before.is_none() {
|
||||||
|
self.grip_dirty_before = Some(self.tabs[i].dirty);
|
||||||
}
|
}
|
||||||
|
|
||||||
let dir = direction / len;
|
if self.grip_originals.is_empty() {
|
||||||
grip.origin_world + dir * dist
|
self.grip_originals = edited_handles
|
||||||
};
|
.iter()
|
||||||
|
.filter_map(|&handle| {
|
||||||
|
self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.get_entity(handle)
|
||||||
|
.cloned()
|
||||||
|
.map(|entity| (handle, entity))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
|
||||||
// The entity is already being edited live. Apply only the
|
self.grip_preview_handles = edited_handles;
|
||||||
// incremental movement from its current grip position to the
|
}
|
||||||
// exact typed-distance position.
|
|
||||||
let delta = target - grip.last_world;
|
let delta = target - grip.last_world;
|
||||||
|
|
||||||
let actions: Vec<_> = grip
|
let actions: Vec<_> = grip
|
||||||
|
|
@ -332,7 +380,9 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||||
let apply = if target_grip.is_translate {
|
let apply = if target_grip.is_translate {
|
||||||
GripApply::Translate(delta)
|
GripApply::Translate(delta)
|
||||||
} else {
|
} else {
|
||||||
GripApply::Absolute(target_grip.last_world + delta)
|
GripApply::Absolute(
|
||||||
|
target_grip.last_world + delta,
|
||||||
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
(
|
(
|
||||||
|
|
@ -349,8 +399,8 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||||
.apply_grip(handle, grip_id, apply);
|
.apply_grip(handle, grip_id, apply);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep the live GripEdit state synchronized so the normal
|
// Keep GripEdit synchronized so the normal commit
|
||||||
// grip commit path sees the exact final position.
|
// path records the exact final position.
|
||||||
if let Some(active) = self.tabs[i].active_grip.as_mut() {
|
if let Some(active) = self.tabs[i].active_grip.as_mut() {
|
||||||
active.last_world = target;
|
active.last_world = target;
|
||||||
|
|
||||||
|
|
@ -361,11 +411,26 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||||
|
|
||||||
self.tabs[i].last_cursor_world = target;
|
self.tabs[i].last_cursor_world = target;
|
||||||
self.command_line.input.clear();
|
self.command_line.input.clear();
|
||||||
|
|
||||||
|
// Consume the typed Dynamic Input values.
|
||||||
|
for field in &mut self.tabs[i].dyn_fields {
|
||||||
|
field.buffer = None;
|
||||||
|
}
|
||||||
|
self.tabs[i].dyn_active = 0;
|
||||||
|
self.dyn_user_reshaped = false;
|
||||||
|
self.dyn_coord_absolute = false;
|
||||||
|
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
|
|
||||||
// Reuse the existing click-move-click grip finalization:
|
// Reuse the existing grip finalization path:
|
||||||
// undo grouping, preview restoration, grip cleanup, etc.
|
// undo grouping, preview restoration, cleanup, etc.
|
||||||
return self.on_viewport_left_release();
|
let task = self.on_viewport_left_release();
|
||||||
|
|
||||||
|
// active_grip is now gone, so remove the temporary
|
||||||
|
// Distance / Angle fields as well.
|
||||||
|
self.sync_dyn_fields();
|
||||||
|
|
||||||
|
return task;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -600,11 +665,26 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||||
if !self.command_line.input.trim().is_empty() {
|
if !self.command_line.input.trim().is_empty() {
|
||||||
return self.update(Message::CommandSubmit);
|
return self.update(Message::CommandSubmit);
|
||||||
}
|
}
|
||||||
// A typed dynamic-input value commits as a point pick
|
// A grip edit is not an active CAD command, but its Dynamic Input
|
||||||
// before the plain-Enter (on_enter) path runs.
|
// fields use the same keyboard path. Route Enter through CommandSubmit,
|
||||||
|
// whose grip branch resolves Distance / Angle and finalizes the edit.
|
||||||
|
let i = self.active_tab;
|
||||||
|
let grip_dyn_locked = self.tabs[i].active_grip.is_some()
|
||||||
|
&& self.dyn_input
|
||||||
|
&& self.tabs[i]
|
||||||
|
.dyn_fields
|
||||||
|
.iter()
|
||||||
|
.any(|field| field.locked());
|
||||||
|
|
||||||
|
if grip_dyn_locked {
|
||||||
|
return self.update(Message::CommandSubmit);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal command Dynamic Input commit.
|
||||||
if let Some(task) = self.try_dyn_commit() {
|
if let Some(task) = self.try_dyn_commit() {
|
||||||
return task;
|
return task;
|
||||||
}
|
}
|
||||||
|
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
if self.tabs[i].active_cmd.is_some() {
|
if self.tabs[i].active_cmd.is_some() {
|
||||||
self.feed_command(crate::command::StepInput::Enter)
|
self.feed_command(crate::command::StepInput::Enter)
|
||||||
|
|
|
||||||
|
|
@ -54,9 +54,55 @@ impl OpenCADStudio {
|
||||||
pub(in crate::app) fn sync_dyn_fields(&mut self) {
|
pub(in crate::app) fn sync_dyn_fields(&mut self) {
|
||||||
use crate::app::document::{DynComponent, DynFieldEntry};
|
use crate::app::document::{DynComponent, DynFieldEntry};
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
if !self.dyn_input || self.tabs[i].active_cmd.is_none() {
|
|
||||||
|
if !self.dyn_input {
|
||||||
self.tabs[i].dyn_fields.clear();
|
self.tabs[i].dyn_fields.clear();
|
||||||
self.tabs[i].dyn_active = 0;
|
self.tabs[i].dyn_active = 0;
|
||||||
|
self.tabs[i].dyn_anchor = None;
|
||||||
|
self.tabs[i].dyn_ref = None;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A normal 2D grip stretch behaves like a point-placement step:
|
||||||
|
// distance and angle are measured from the grip's original position.
|
||||||
|
//
|
||||||
|
// Grip editing is not an `active_cmd`, so handle it before the normal
|
||||||
|
// command-only path below.
|
||||||
|
let grip_origin = self.tabs[i]
|
||||||
|
.active_grip
|
||||||
|
.as_ref()
|
||||||
|
.filter(|grip| {
|
||||||
|
grip.mode == crate::scene::pick::grip::GripEditMode::Stretch
|
||||||
|
})
|
||||||
|
.map(|grip| grip.origin_world);
|
||||||
|
|
||||||
|
if let Some(origin) = grip_origin {
|
||||||
|
let wanted = [DynComponent::Distance, DynComponent::Angle];
|
||||||
|
let current: Vec<DynComponent> = self.tabs[i]
|
||||||
|
.dyn_fields
|
||||||
|
.iter()
|
||||||
|
.map(|field| field.component)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if current.as_slice() != wanted {
|
||||||
|
self.tabs[i].dyn_fields = wanted
|
||||||
|
.into_iter()
|
||||||
|
.map(DynFieldEntry::new)
|
||||||
|
.collect();
|
||||||
|
self.tabs[i].dyn_active = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.tabs[i].dyn_guide = crate::command::DynGuide::Polar;
|
||||||
|
self.tabs[i].dyn_anchor = Some(origin);
|
||||||
|
self.tabs[i].dyn_ref = None;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.tabs[i].active_cmd.is_none() {
|
||||||
|
self.tabs[i].dyn_fields.clear();
|
||||||
|
self.tabs[i].dyn_active = 0;
|
||||||
|
self.tabs[i].dyn_anchor = None;
|
||||||
|
self.tabs[i].dyn_ref = None;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// A command may describe its step explicitly via `dyn_spec()` — that
|
// A command may describe its step explicitly via `dyn_spec()` — that
|
||||||
|
|
|
||||||
|
|
@ -1087,7 +1087,12 @@ impl OpenCADStudio {
|
||||||
// dimension re-tessellates) and a Square insertion grip (so
|
// dimension re-tessellates) and a Square insertion grip (so
|
||||||
// an MTEXT width handle, a Triangle, still re-tessellates so
|
// an MTEXT width handle, a Triangle, still re-tessellates so
|
||||||
// the re-wrap is exact).
|
// the re-wrap is exact).
|
||||||
let snap = self.tabs[i].scene.wire_models_for(&edited_handles);
|
let snap = self.tabs[i].scene.wire_models_for(&edited_handles);
|
||||||
|
|
||||||
|
// Keep the entity's original geometry available for self-OSNAP,
|
||||||
|
// Extension and OTRACK while the live grip preview is being deformed.
|
||||||
|
self.grip_snap_wires = snap.clone();
|
||||||
|
|
||||||
self.grip_text_verts = snap
|
self.grip_text_verts = snap
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|w| w.text_verts.iter().copied())
|
.flat_map(|w| w.text_verts.iter().copied())
|
||||||
|
|
@ -1132,6 +1137,19 @@ impl OpenCADStudio {
|
||||||
bounds,
|
bounds,
|
||||||
self.snapper.osnap_radius_px,
|
self.snapper.osnap_radius_px,
|
||||||
);
|
);
|
||||||
|
// `snap_candidates` contains only the spatially-local wires from the rest of
|
||||||
|
// the drawing. Add the frozen pre-drag geometry of the edited entity so its
|
||||||
|
// own vertices and segments remain valid snap references.
|
||||||
|
//
|
||||||
|
// This intentionally uses a plain Vec<WireModel>: Snapper will use its normal
|
||||||
|
// unindexed fallback over this already-small local candidate set rather than
|
||||||
|
// cloning/scanning the whole drawing.
|
||||||
|
let mut grip_snap_candidates: Vec<_> =
|
||||||
|
snap_candidates.iter().cloned().collect();
|
||||||
|
|
||||||
|
grip_snap_candidates.extend(
|
||||||
|
self.grip_snap_wires.iter().cloned()
|
||||||
|
);
|
||||||
// The engaged grip is the rubber-band origin. Perpendicular
|
// The engaged grip is the rubber-band origin. Perpendicular
|
||||||
// snapping must drop its foot from this point, including when a
|
// snapping must drop its foot from this point, including when a
|
||||||
// hot-grip set is moved by the same drag vector.
|
// hot-grip set is moved by the same drag vector.
|
||||||
|
|
@ -1145,7 +1163,7 @@ impl OpenCADStudio {
|
||||||
let snap_hit = self.snapper.snap(
|
let snap_hit = self.snapper.snap(
|
||||||
raw,
|
raw,
|
||||||
p,
|
p,
|
||||||
&snap_candidates,
|
&grip_snap_candidates,
|
||||||
view_rot,
|
view_rot,
|
||||||
eye,
|
eye,
|
||||||
bounds,
|
bounds,
|
||||||
|
|
@ -1158,7 +1176,7 @@ impl OpenCADStudio {
|
||||||
|
|
||||||
self.snapper.update_otrack_dwell(
|
self.snapper.update_otrack_dwell(
|
||||||
snap_hit,
|
snap_hit,
|
||||||
&snap_candidates,
|
&grip_snap_candidates,
|
||||||
view_rot,
|
view_rot,
|
||||||
eye,
|
eye,
|
||||||
bounds,
|
bounds,
|
||||||
|
|
@ -1230,6 +1248,17 @@ impl OpenCADStudio {
|
||||||
// by the grip, otherwise the guide and the edited geometry diverge.
|
// by the grip, otherwise the guide and the edited geometry diverge.
|
||||||
self.tabs[i].last_cursor_world = snapped;
|
self.tabs[i].last_cursor_world = snapped;
|
||||||
self.tabs[i].last_cursor_screen = p_full;
|
self.tabs[i].last_cursor_screen = p_full;
|
||||||
|
|
||||||
|
// Project the grip's original position into full-canvas coordinates.
|
||||||
|
// Dynamic Input uses this as the polar Distance/Angle anchor.
|
||||||
|
let anchor_ndc =
|
||||||
|
view_rot.project_point3((grip.origin_world - eye).as_vec3());
|
||||||
|
|
||||||
|
self.tabs[i].last_point_screen = Some(Point::new(
|
||||||
|
(anchor_ndc.x + 1.0) * 0.5 * bounds.width + tile_b.x,
|
||||||
|
(1.0 - anchor_ndc.y) * 0.5 * bounds.height + tile_b.y,
|
||||||
|
));
|
||||||
|
|
||||||
let apply_started = Instant::now();
|
let apply_started = Instant::now();
|
||||||
let delta = snapped - grip.last_world;
|
let delta = snapped - grip.last_world;
|
||||||
let lengthen = grip.mode == GripEditMode::Lengthen;
|
let lengthen = grip.mode == GripEditMode::Lengthen;
|
||||||
|
|
@ -2587,6 +2616,11 @@ impl OpenCADStudio {
|
||||||
));
|
));
|
||||||
self.grip_hover = None;
|
self.grip_hover = None;
|
||||||
self.grip_popup = None;
|
self.grip_popup = None;
|
||||||
|
|
||||||
|
// A grip edit is not a CAD command, so explicitly seed the shared
|
||||||
|
// dynamic-input fields for the newly engaged grip.
|
||||||
|
self.sync_dyn_fields();
|
||||||
|
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2705,6 +2739,7 @@ impl OpenCADStudio {
|
||||||
);
|
);
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
}
|
}
|
||||||
|
self.grip_snap_wires.clear();
|
||||||
self.grip_text_verts = Vec::new();
|
self.grip_text_verts = Vec::new();
|
||||||
self.grip_text_slide = false;
|
self.grip_text_slide = false;
|
||||||
for &handle in &handles {
|
for &handle in &handles {
|
||||||
|
|
|
||||||
|
|
@ -757,11 +757,11 @@ impl OpenCADStudio {
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
let dyn_input_overlay: Option<Element<'_, Message>> =
|
let dyn_input_overlay: Option<Element<'_, Message>> =
|
||||||
if self.dyn_input
|
if self.dyn_input
|
||||||
&& tab.active_cmd.is_some()
|
&& (tab.active_cmd.is_some() || tab.active_grip.is_some())
|
||||||
&& (!tab.dyn_fields.is_empty() || dyn_picks_object)
|
&& (!tab.dyn_fields.is_empty() || dyn_picks_object)
|
||||||
{
|
{
|
||||||
let w = tab.last_cursor_world;
|
let w = tab.last_cursor_world;
|
||||||
let base = self.last_point;
|
let base = tab.dyn_anchor.or(self.last_point);
|
||||||
// A command may drive a typed scalar by mouse (e.g. a
|
// A command may drive a typed scalar by mouse (e.g. a
|
||||||
// perpendicular distance to a picked object); show that live
|
// perpendicular distance to a picked object); show that live
|
||||||
// value in the box until the user types over it.
|
// value in the box until the user types over it.
|
||||||
|
|
@ -1536,7 +1536,9 @@ impl OpenCADStudio {
|
||||||
// The MText preview also captures keystrokes (typing edits it), so the
|
// The MText preview also captures keystrokes (typing edits it), so the
|
||||||
// command line must likewise release its on_input there.
|
// command line must likewise release its on_input there.
|
||||||
let dyn_capturing =
|
let dyn_capturing =
|
||||||
(self.dyn_input && tab.active_cmd.is_some() && !tab.dyn_fields.is_empty())
|
(self.dyn_input
|
||||||
|
&& (tab.active_cmd.is_some() || tab.active_grip.is_some())
|
||||||
|
&& !tab.dyn_fields.is_empty())
|
||||||
|| self.mtext_editor.as_ref().is_some_and(|e| e.show_preview)
|
|| self.mtext_editor.as_ref().is_some_and(|e| e.show_preview)
|
||||||
|| self.text_inline.is_some();
|
|| self.text_inline.is_some();
|
||||||
let workspace: Element<'_, Message> = match (properties_el, self.properties_side) {
|
let workspace: Element<'_, Message> = match (properties_el, self.properties_side) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue