Free mode: Z index positioning of blocks.
This commit is contained in:
parent
4e3dfd8e85
commit
32c66cd330
|
|
@ -1,4 +1,4 @@
|
|||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicU8, Ordering};
|
||||
use std::time::Instant;
|
||||
|
|
@ -179,6 +179,10 @@ pub enum Message {
|
|||
ImagePromotePress { block_id: crate::selection::BlockId, after_line: usize, src: String },
|
||||
/// mouse released after a block or image promote press.
|
||||
PromoteRelease,
|
||||
/// mouse pressed on a resize band of a free-layer object.
|
||||
ResizePress { node_id: FreeNodeId, horiz: bool, vert: bool },
|
||||
/// mouse released after a resize press.
|
||||
ResizeRelease,
|
||||
ToggleMenu(MenuCategory),
|
||||
CloseMenu,
|
||||
Shell(ShellAction),
|
||||
|
|
@ -348,6 +352,16 @@ pub struct PromoteDragState {
|
|||
pub fallback_table_idx: Option<usize>,
|
||||
}
|
||||
|
||||
/// pending drag state for resizing a free-layer object from an edge band.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ResizeDragState {
|
||||
pub node_id: FreeNodeId,
|
||||
pub start_cursor: Point,
|
||||
pub start_size: (f32, f32),
|
||||
pub axes: (bool, bool),
|
||||
pub snapshot_pushed: bool,
|
||||
}
|
||||
|
||||
impl LayerItem<'_> {
|
||||
fn element_height(&self, line_h: f32, font_size: f32) -> f32 {
|
||||
match self {
|
||||
|
|
@ -370,6 +384,8 @@ struct UndoSnapshot {
|
|||
text: String,
|
||||
cursor_line: usize,
|
||||
cursor_col: usize,
|
||||
free_placements: HashMap<FreeNodeId, FreePlacement>,
|
||||
frozen_doc_size: Option<(f32, f32)>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Copy)]
|
||||
|
|
@ -478,6 +494,9 @@ pub struct EditorState {
|
|||
pub frozen_doc_size: Option<(f32, f32)>,
|
||||
pub viewport_size: (f32, f32),
|
||||
pub promote_drag: Option<PromoteDragState>,
|
||||
pub promote_snapshot_pushed: bool,
|
||||
pub resize_drag: Option<ResizeDragState>,
|
||||
pub active_free: Option<FreeNodeId>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
@ -598,6 +617,9 @@ impl EditorState {
|
|||
frozen_doc_size: None,
|
||||
viewport_size: (0.0, 0.0),
|
||||
promote_drag: None,
|
||||
promote_snapshot_pushed: false,
|
||||
resize_drag: None,
|
||||
active_free: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -614,6 +636,11 @@ impl EditorState {
|
|||
if dist_sq < threshold_sq && !was_escalated {
|
||||
return false;
|
||||
}
|
||||
if !was_escalated && !self.promote_snapshot_pushed {
|
||||
self.push_undo_snapshot();
|
||||
self.redo_stack.clear();
|
||||
self.promote_snapshot_pushed = true;
|
||||
}
|
||||
if let Some(pd) = self.promote_drag.as_mut() { pd.escalated = true; }
|
||||
let placement = FreePlacement {
|
||||
layer,
|
||||
|
|
@ -629,15 +656,111 @@ impl EditorState {
|
|||
true
|
||||
}
|
||||
|
||||
/// applies the current cursor delta to any active resize drag.
|
||||
pub fn tick_resize_drag(&mut self) -> bool {
|
||||
let (node_id, dx, dy, start_size, axes, snapshot_pushed) = {
|
||||
let Some(rd) = self.resize_drag.as_ref() else { return false };
|
||||
let dx = self.cursor_pos.x - rd.start_cursor.x;
|
||||
let dy = self.cursor_pos.y - rd.start_cursor.y;
|
||||
(rd.node_id.clone(), dx, dy, rd.start_size, rd.axes, rd.snapshot_pushed)
|
||||
};
|
||||
let new_w = if axes.0 { (start_size.0 + dx).max(60.0) } else { start_size.0 };
|
||||
let new_h = if axes.1 { (start_size.1 + dy).max(40.0) } else { start_size.1 };
|
||||
let changed = self
|
||||
.free_placements
|
||||
.get(&node_id)
|
||||
.map(|p| (p.w - new_w).abs() > 0.5 || (p.h - new_h).abs() > 0.5)
|
||||
.unwrap_or(false);
|
||||
if !changed { return false; }
|
||||
if !snapshot_pushed {
|
||||
self.push_undo_snapshot();
|
||||
self.redo_stack.clear();
|
||||
if let Some(rd) = self.resize_drag.as_mut() { rd.snapshot_pushed = true; }
|
||||
}
|
||||
if let Some(p) = self.free_placements.get_mut(&node_id) {
|
||||
p.w = new_w;
|
||||
p.h = new_h;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// returns the layout block ids belonging to the module anchored by the given block.
|
||||
pub fn module_block_ids(&self, anchor: crate::selection::BlockId) -> Vec<crate::selection::BlockId> {
|
||||
let Some(start) = self.layout.iter().position(|id| *id == anchor) else { return Vec::new() };
|
||||
let anchor_block = match self.registry.get(&anchor) {
|
||||
Some(b) => b,
|
||||
None => return Vec::new(),
|
||||
};
|
||||
let any = anchor_block.as_any();
|
||||
let is_module_anchor = any.is::<HeadingBlock>() || any.is::<HrBlock>();
|
||||
if !is_module_anchor {
|
||||
return vec![anchor];
|
||||
}
|
||||
let mut ids = vec![anchor];
|
||||
for &next_id in &self.layout[start + 1..] {
|
||||
let Some(block) = self.registry.get(&next_id) else { break };
|
||||
let any = block.as_any();
|
||||
if any.is::<HeadingBlock>() || any.is::<HrBlock>() {
|
||||
break;
|
||||
}
|
||||
ids.push(next_id);
|
||||
}
|
||||
ids
|
||||
}
|
||||
|
||||
/// returns the layer-0 natural size for a given free-layer node.
|
||||
pub fn natural_size_for_node(&self, node_id: &FreeNodeId) -> (f32, f32) {
|
||||
let viewport_w = if self.viewport_size.0 > 0.0 { self.viewport_size.0 } else { 800.0 };
|
||||
let default_w = viewport_w.min(700.0).max(300.0);
|
||||
match node_id {
|
||||
FreeNodeId::Block(block_id) => {
|
||||
let module = self.module_block_ids(*block_id);
|
||||
let mut total_h: f32 = 0.0;
|
||||
let mut max_w: f32 = default_w;
|
||||
for id in &module {
|
||||
let Some(block) = self.registry.get(id) else { continue };
|
||||
let any = block.as_any();
|
||||
if let Some(tab) = any.downcast_ref::<TableBlock>() {
|
||||
let w: f32 = tab.col_widths.iter().sum::<f32>() + 60.0;
|
||||
let h = (tab.rows.len().max(1) as f32) * (self.font_size * 1.6) + 16.0;
|
||||
total_h += h;
|
||||
if w > max_w { max_w = w; }
|
||||
} else if any.is::<HeadingBlock>() {
|
||||
total_h += self.font_size * 2.4 + 8.0;
|
||||
} else if any.is::<HrBlock>() {
|
||||
total_h += 24.0;
|
||||
} else if let Some(tb) = any.downcast_ref::<TextBlock>() {
|
||||
let lc = tb.content.line_count() as f32;
|
||||
total_h += lc * self.font_size * 1.3 + 16.0;
|
||||
}
|
||||
}
|
||||
(max_w, total_h.max(80.0))
|
||||
}
|
||||
FreeNodeId::Image(block_id, after_line, _src) => {
|
||||
let img = self.computed_images.iter().find(|i|
|
||||
i.anchor.block_id == *block_id && i.anchor.after_line == *after_line
|
||||
);
|
||||
match img {
|
||||
Some(img) => (img.display_height * 4.0 / 3.0, img.display_height),
|
||||
None => (default_w, 200.0),
|
||||
}
|
||||
}
|
||||
_ => (default_w, 200.0),
|
||||
}
|
||||
}
|
||||
|
||||
/// arms a drag promotion for any free-layer node in live mode.
|
||||
fn start_promote(&mut self, node_id: FreeNodeId, fallback_table_idx: Option<usize>) {
|
||||
if !matches!(self.render_mode, RenderMode::Live) { return; }
|
||||
let existing = self.free_placements.get(&node_id).copied();
|
||||
let default_size = (360.0, 240.0);
|
||||
let (origin, size, layer) = match existing {
|
||||
Some(p) => ((p.x, p.y), (p.w, p.h), p.layer),
|
||||
None => ((self.cursor_pos.x, self.cursor_pos.y), default_size, 1),
|
||||
None => {
|
||||
let s = self.natural_size_for_node(&node_id);
|
||||
((self.cursor_pos.x, self.cursor_pos.y), s, 1)
|
||||
}
|
||||
};
|
||||
self.active_free = Some(node_id.clone());
|
||||
self.promote_drag = Some(PromoteDragState {
|
||||
node_id,
|
||||
start_cursor: self.cursor_pos,
|
||||
|
|
@ -647,6 +770,7 @@ impl EditorState {
|
|||
escalated: false,
|
||||
fallback_table_idx,
|
||||
});
|
||||
self.promote_snapshot_pushed = false;
|
||||
}
|
||||
|
||||
/// arms a corner-drag promotion for the table at a layout index.
|
||||
|
|
@ -2448,6 +2572,8 @@ impl EditorState {
|
|||
text: self.get_clean_text(),
|
||||
cursor_line: cursor.position.line,
|
||||
cursor_col: cursor.position.column,
|
||||
free_placements: self.free_placements.clone(),
|
||||
frozen_doc_size: self.frozen_doc_size,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2497,6 +2623,8 @@ impl EditorState {
|
|||
position: Position { line: snap.cursor_line, column: snap.cursor_col },
|
||||
selection: None,
|
||||
});
|
||||
self.free_placements = snap.free_placements.clone();
|
||||
self.frozen_doc_size = snap.frozen_doc_size;
|
||||
}
|
||||
|
||||
fn perform_undo(&mut self) {
|
||||
|
|
@ -3447,6 +3575,21 @@ impl EditorState {
|
|||
Message::PromoteRelease => {
|
||||
self.promote_drag = None;
|
||||
}
|
||||
Message::ResizePress { node_id, horiz, vert } => {
|
||||
if !matches!(self.render_mode, RenderMode::Live) { return; }
|
||||
if self.active_free.as_ref() != Some(&node_id) { return; }
|
||||
let Some(p) = self.free_placements.get(&node_id).copied() else { return };
|
||||
self.resize_drag = Some(ResizeDragState {
|
||||
node_id,
|
||||
start_cursor: self.cursor_pos,
|
||||
start_size: (p.w, p.h),
|
||||
axes: (horiz, vert),
|
||||
snapshot_pushed: false,
|
||||
});
|
||||
}
|
||||
Message::ResizeRelease => {
|
||||
self.resize_drag = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3644,9 +3787,19 @@ impl EditorState {
|
|||
|
||||
let lang_for_block = self.lang_str();
|
||||
|
||||
let hidden_blocks: HashSet<crate::selection::BlockId> = self
|
||||
.free_placements
|
||||
.keys()
|
||||
.filter_map(|id| match id {
|
||||
FreeNodeId::Block(bid) => Some(*bid),
|
||||
_ => None,
|
||||
})
|
||||
.flat_map(|bid| self.module_block_ids(bid))
|
||||
.collect();
|
||||
|
||||
let mut global_line = 0usize;
|
||||
for (bi, &block_id) in self.layout.iter().enumerate() {
|
||||
if self.free_placements.contains_key(&FreeNodeId::Block(block_id)) {
|
||||
if hidden_blocks.contains(&block_id) {
|
||||
continue;
|
||||
}
|
||||
let block = self.registry.get(&block_id).unwrap();
|
||||
|
|
@ -3708,59 +3861,12 @@ impl EditorState {
|
|||
block_elements.push(editor_el);
|
||||
} else {
|
||||
let top_pad = if bi == 0 { title_bar_h } else { 0.0 };
|
||||
let is_focused = bi == self.focused_block;
|
||||
let anchored_items = self.build_anchored_items(tb.id);
|
||||
let cursor_line = tb.content.cursor().position.line;
|
||||
let line_count = tb.content.line_count();
|
||||
let text = tb.content.text();
|
||||
let decors = compute_line_decors(&text);
|
||||
let this_global_line = global_line;
|
||||
global_line += line_count;
|
||||
let _ = line_h; // text_widget::layout owns the height now
|
||||
|
||||
let editor = text_widget::TextEditor::new(&tb.content)
|
||||
.id(block_editor_id(tb.id))
|
||||
.on_action(move |action| Message::BlockAction(block_idx, action))
|
||||
.font(syntax::EDITOR_FONT)
|
||||
.size(self.font_size)
|
||||
.height(Length::Shrink)
|
||||
.padding(Padding { top: top_pad, right: 8.0, bottom: 4.0, left: 8.0 })
|
||||
.wrapping(Wrapping::Word)
|
||||
.key_binding(macos_key_binding)
|
||||
.anchored(anchored_items)
|
||||
.show_gutter(true)
|
||||
.gutter_offset(this_global_line)
|
||||
.focused(is_focused)
|
||||
.cursor_line(if is_focused { Some(cursor_line) } else { None })
|
||||
.line_indicator(self.line_indicator)
|
||||
.gutter_rainbow(self.gutter_rainbow)
|
||||
.line_decors(decors)
|
||||
.style(|_theme, _status| {
|
||||
let p = palette::current();
|
||||
text_widget::Style {
|
||||
background: Background::Color(p.base),
|
||||
border: Border::default(),
|
||||
placeholder: p.overlay0,
|
||||
value: p.text,
|
||||
selection: Color { a: 0.4, ..p.blue },
|
||||
}
|
||||
});
|
||||
|
||||
let settings = SyntaxSettings {
|
||||
lang: lang_for_block.clone(),
|
||||
source: tb.content.text(),
|
||||
};
|
||||
let editor_el: Element<'_, Message, Theme, iced_wgpu::Renderer> = editor
|
||||
.highlight_with::<SyntaxHighlighter>(
|
||||
settings,
|
||||
|highlight, _theme| Format {
|
||||
color: Some(syntax::highlight_color(highlight.kind)),
|
||||
font: syntax::highlight_font(highlight.kind),
|
||||
},
|
||||
)
|
||||
.into();
|
||||
|
||||
block_elements.push(editor_el);
|
||||
let _ = lang_for_block; // build_text_block_widget reads lang_str directly
|
||||
block_elements.push(self.build_text_block_widget(tb, block_idx, this_global_line, top_pad));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
|
@ -4125,6 +4231,64 @@ impl EditorState {
|
|||
anchored
|
||||
}
|
||||
|
||||
/// builds the text-editor widget for a text block at a layout index.
|
||||
fn build_text_block_widget<'a>(
|
||||
&'a self,
|
||||
tb: &'a TextBlock,
|
||||
block_idx: usize,
|
||||
this_global_line: usize,
|
||||
top_pad: f32,
|
||||
) -> Element<'a, Message, Theme, iced_wgpu::Renderer> {
|
||||
let is_focused = block_idx == self.focused_block;
|
||||
let anchored_items = self.build_anchored_items(tb.id);
|
||||
let cursor_line = tb.content.cursor().position.line;
|
||||
let text = tb.content.text();
|
||||
let decors = compute_line_decors(&text);
|
||||
let lang_for_block = self.lang_str();
|
||||
|
||||
let editor = text_widget::TextEditor::new(&tb.content)
|
||||
.id(block_editor_id(tb.id))
|
||||
.on_action(move |action| Message::BlockAction(block_idx, action))
|
||||
.font(syntax::EDITOR_FONT)
|
||||
.size(self.font_size)
|
||||
.height(Length::Shrink)
|
||||
.padding(Padding { top: top_pad, right: 8.0, bottom: 4.0, left: 8.0 })
|
||||
.wrapping(Wrapping::Word)
|
||||
.key_binding(macos_key_binding)
|
||||
.anchored(anchored_items)
|
||||
.show_gutter(true)
|
||||
.gutter_offset(this_global_line)
|
||||
.focused(is_focused)
|
||||
.cursor_line(if is_focused { Some(cursor_line) } else { None })
|
||||
.line_indicator(self.line_indicator)
|
||||
.gutter_rainbow(self.gutter_rainbow)
|
||||
.line_decors(decors)
|
||||
.style(|_theme, _status| {
|
||||
let p = palette::current();
|
||||
text_widget::Style {
|
||||
background: Background::Color(p.base),
|
||||
border: Border::default(),
|
||||
placeholder: p.overlay0,
|
||||
value: p.text,
|
||||
selection: Color { a: 0.4, ..p.blue },
|
||||
}
|
||||
});
|
||||
|
||||
let settings = SyntaxSettings {
|
||||
lang: lang_for_block,
|
||||
source: tb.content.text(),
|
||||
};
|
||||
editor
|
||||
.highlight_with::<SyntaxHighlighter>(
|
||||
settings,
|
||||
|highlight, _theme| Format {
|
||||
color: Some(syntax::highlight_color(highlight.kind)),
|
||||
font: syntax::highlight_font(highlight.kind),
|
||||
},
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
/// builds a column of cell rows from a computed table.
|
||||
fn build_computed_table_widget<'a>(
|
||||
&self,
|
||||
|
|
@ -4206,6 +4370,9 @@ impl EditorState {
|
|||
let bi = self.layout.iter().position(|id| *id == block_id)?;
|
||||
let block = self.registry.get(&block_id)?;
|
||||
let any = block.as_any();
|
||||
if let Some(tb) = any.downcast_ref::<TextBlock>() {
|
||||
return Some(self.build_text_block_widget(tb, bi, 0, 0.0));
|
||||
}
|
||||
if let Some(tab) = any.downcast_ref::<TableBlock>() {
|
||||
let editing_cell = match self.editing.as_ref() {
|
||||
Some(path) if path.block_id == tab.id => match &path.inner {
|
||||
|
|
@ -4246,6 +4413,51 @@ impl EditorState {
|
|||
None
|
||||
}
|
||||
|
||||
/// builds a column of overlay widgets for every block in a module.
|
||||
fn build_free_module_widget(
|
||||
&self,
|
||||
anchor: crate::selection::BlockId,
|
||||
) -> Option<Element<'_, Message, Theme, iced_wgpu::Renderer>> {
|
||||
let ids = self.module_block_ids(anchor);
|
||||
if ids.is_empty() { return None; }
|
||||
if ids.len() == 1 {
|
||||
return self.build_free_block_widget(ids[0]);
|
||||
}
|
||||
let parts: Vec<Element<'_, Message, Theme, iced_wgpu::Renderer>> = ids
|
||||
.iter()
|
||||
.filter_map(|id| self.build_free_block_widget(*id))
|
||||
.collect();
|
||||
if parts.is_empty() { return None; }
|
||||
Some(iced_widget::column(parts).into())
|
||||
}
|
||||
|
||||
/// builds a single resize band aligned to one or two edges of a placement.
|
||||
fn build_resize_band(
|
||||
&self,
|
||||
node_id: FreeNodeId,
|
||||
horiz: bool,
|
||||
vert: bool,
|
||||
align_x: alignment::Horizontal,
|
||||
align_y: alignment::Vertical,
|
||||
zone_w: Length,
|
||||
zone_h: Length,
|
||||
) -> Element<'_, Message, Theme, iced_wgpu::Renderer> {
|
||||
let zone = iced_widget::container(
|
||||
iced_widget::Space::new().width(Length::Fill).height(Length::Fill)
|
||||
)
|
||||
.width(zone_w)
|
||||
.height(zone_h);
|
||||
let area = MouseArea::new(zone)
|
||||
.on_press(Message::ResizePress { node_id, horiz, vert })
|
||||
.on_release(Message::ResizeRelease);
|
||||
iced_widget::container(area)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.align_x(align_x)
|
||||
.align_y(align_y)
|
||||
.into()
|
||||
}
|
||||
|
||||
/// stacks free-placed objects at absolute positions over the editor body.
|
||||
fn build_free_overlay(&self) -> Option<Element<'_, Message, Theme, iced_wgpu::Renderer>> {
|
||||
if self.free_placements.is_empty() {
|
||||
|
|
@ -4263,7 +4475,7 @@ impl EditorState {
|
|||
.find(|ct| ct.anchor.block_id == *block_id && ct.anchor.after_line == *after_line)
|
||||
.map(|ct| self.build_computed_table_widget(ct)),
|
||||
FreeNodeId::Block(block_id) => self
|
||||
.build_free_block_widget(*block_id)
|
||||
.build_free_module_widget(*block_id)
|
||||
.map(|el| self.wrap_block_with_promote(el, *block_id)),
|
||||
FreeNodeId::Image(block_id, after_line, src) => self
|
||||
.computed_images
|
||||
|
|
@ -4297,14 +4509,44 @@ impl EditorState {
|
|||
};
|
||||
let Some(inner) = inner_opt else { continue };
|
||||
|
||||
let sized = iced_widget::container(iced_widget::scrollable(inner))
|
||||
let is_active = self.active_free.as_ref() == Some(id);
|
||||
let band = (self.font_size * 1.3) * 0.5;
|
||||
let body: Element<'_, Message, Theme, iced_wgpu::Renderer> = if is_active {
|
||||
let right = self.build_resize_band(
|
||||
id.clone(),
|
||||
true, false,
|
||||
alignment::Horizontal::Right, alignment::Vertical::Top,
|
||||
Length::Fixed(band), Length::Fixed((placement.h - band).max(0.0)),
|
||||
);
|
||||
let bottom = self.build_resize_band(
|
||||
id.clone(),
|
||||
false, true,
|
||||
alignment::Horizontal::Left, alignment::Vertical::Bottom,
|
||||
Length::Fixed((placement.w - band).max(0.0)), Length::Fixed(band),
|
||||
);
|
||||
let corner = self.build_resize_band(
|
||||
id.clone(),
|
||||
true, true,
|
||||
alignment::Horizontal::Right, alignment::Vertical::Bottom,
|
||||
Length::Fixed(band), Length::Fixed(band),
|
||||
);
|
||||
iced_widget::stack![iced_widget::scrollable(inner), right, bottom, corner].into()
|
||||
} else {
|
||||
iced_widget::scrollable(inner).into()
|
||||
};
|
||||
let sized = iced_widget::container(body)
|
||||
.width(Length::Fixed(placement.w))
|
||||
.height(Length::Fixed(placement.h))
|
||||
.style(|_theme: &Theme| {
|
||||
.style(move |_theme: &Theme| {
|
||||
let p = palette::current();
|
||||
let (border_color, border_w) = if is_active {
|
||||
(p.blue, 2.0)
|
||||
} else {
|
||||
(p.surface1, 1.0)
|
||||
};
|
||||
container::Style {
|
||||
background: Some(Background::Color(p.base)),
|
||||
border: Border { color: p.surface1, width: 1.0, radius: 4.0.into() },
|
||||
border: Border { color: border_color, width: border_w, radius: 4.0.into() },
|
||||
text_color: None,
|
||||
shadow: Shadow::default(),
|
||||
snap: false,
|
||||
|
|
|
|||
|
|
@ -753,6 +753,9 @@ pub fn render(handle: &mut ViewportHandle) {
|
|||
if handle.state.tick_promote_drag() {
|
||||
handle.needs_redraw = true;
|
||||
}
|
||||
if handle.state.tick_resize_drag() {
|
||||
handle.needs_redraw = true;
|
||||
}
|
||||
}
|
||||
handle.state.sync_focused_cell(focused_id.as_ref());
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue