Modes menu group and submenu items:

Free (prettyu much implemented)
Relative (also pretty close)
Anchored (will take more than the afformentioned two, more to come)
This commit is contained in:
jess 2026-05-10 00:36:18 -07:00
parent 32c66cd330
commit 62f5d6212e
5 changed files with 150 additions and 5 deletions

View File

@ -171,6 +171,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuItemValidation {
mainMenu.addItem(buildFileMenu())
mainMenu.addItem(buildEditMenu())
mainMenu.addItem(buildRenderMenu())
mainMenu.addItem(buildModeMenu())
mainMenu.addItem(buildViewMenu())
mainMenu.addItem(buildWindowMenu())
@ -345,6 +346,32 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuItemValidation {
return item
}
private func buildModeMenu() -> NSMenuItem {
let item = NSMenuItem()
let menu = NSMenu(title: "Mode")
let freeItem = NSMenuItem(title: "Free", action: #selector(setLayoutModeFree), keyEquivalent: "")
freeItem.target = self
menu.addItem(freeItem)
let relativeItem = NSMenuItem(title: "Relative", action: #selector(setLayoutModeRelative), keyEquivalent: "")
relativeItem.target = self
menu.addItem(relativeItem)
let anchoredItem = NSMenuItem(title: "Anchored", action: #selector(setLayoutModeAnchored), keyEquivalent: "")
anchoredItem.target = self
menu.addItem(anchoredItem)
menu.addItem(.separator())
let snappingItem = NSMenuItem(title: "Snapping", action: #selector(toggleSnapping), keyEquivalent: "")
snappingItem.target = self
menu.addItem(snappingItem)
item.submenu = menu
return item
}
private func buildViewMenu() -> NSMenuItem {
let item = NSMenuItem()
let menu = NSMenu(title: "View")
@ -647,6 +674,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuItemValidation {
func validateMenuItem(_ menuItem: NSMenuItem) -> Bool {
let mode = viewport?.renderMode() ?? 0
let layout = viewport?.layoutMode() ?? 0
switch menuItem.action {
case #selector(setLiveMode):
menuItem.state = mode == 0 ? .on : .off
@ -654,6 +682,14 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuItemValidation {
menuItem.state = mode == 1 ? .on : .off
case #selector(setViewMode):
menuItem.state = mode == 2 ? .on : .off
case #selector(setLayoutModeFree):
menuItem.state = layout == 0 ? .on : .off
case #selector(setLayoutModeRelative):
menuItem.state = layout == 1 ? .on : .off
case #selector(setLayoutModeAnchored):
menuItem.state = layout == 2 ? .on : .off
case #selector(toggleSnapping):
menuItem.state = (viewport?.snapping() ?? false) ? .on : .off
default:
break
}
@ -676,6 +712,22 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuItemValidation {
viewport?.sendCommand(13)
}
@objc private func setLayoutModeFree() {
viewport?.sendCommand(17)
}
@objc private func setLayoutModeRelative() {
viewport?.sendCommand(18)
}
@objc private func setLayoutModeAnchored() {
viewport?.sendCommand(19)
}
@objc private func toggleSnapping() {
viewport?.sendCommand(20)
}
@objc private func formatDocument() {
viewport?.sendCommand(10)
}

View File

@ -298,6 +298,17 @@ class IcedViewportView: NSView {
return viewport_render_mode(h)
}
/// Returns 0 = Free, 1 = Relative, 2 = Anchored.
func layoutMode() -> UInt32 {
guard let h = viewportHandle else { return 0 }
return viewport_layout_mode(h)
}
func snapping() -> Bool {
guard let h = viewportHandle else { return false }
return viewport_snapping(h)
}
func setSettingsView(themeMode: String, lineIndicator: String, gutterRainbow: Bool, autoSaveDir: String) {
guard let h = viewportHandle else { return }
themeMode.withCString { t in

View File

@ -173,4 +173,14 @@ void browser_send_command(struct BrowserHandle *handle, uint32_t command);
uint32_t viewport_render_mode(struct ViewportHandle *handle);
/**
* returns 0 for free, 1 for relative, 2 for anchored layout mode.
*/
uint32_t viewport_layout_mode(struct ViewportHandle *handle);
/**
* returns true when 0.25-line snapping is on for drags and resizes.
*/
bool viewport_snapping(struct ViewportHandle *handle);
#endif /* ACORD_VIEWPORT_H */

View File

@ -67,6 +67,16 @@ pub enum RenderMode {
View,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LayoutMode {
/// objects float on layers above zero with absolute coordinates
Free,
/// objects stay on layer 0 and reorder by drag
Relative,
/// objects sit at fixed positions and layer 0 wraps around the cutouts
Anchored,
}
/// gutter line-number and cursorline display mode
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LineIndicator {
@ -183,6 +193,10 @@ pub enum Message {
ResizePress { node_id: FreeNodeId, horiz: bool, vert: bool },
/// mouse released after a resize press.
ResizeRelease,
/// switches between free, relative, and anchored layout modes.
SetLayoutMode(LayoutMode),
/// toggles 0.25-line snap on drags and resizes.
ToggleSnapping,
ToggleMenu(MenuCategory),
CloseMenu,
Shell(ShellAction),
@ -193,6 +207,7 @@ pub enum MenuCategory {
File,
Edit,
Render,
Mode,
View,
}
@ -214,10 +229,11 @@ pub enum ShellAction {
}
#[cfg(any(target_os = "linux", target_os = "windows"))]
const MENU_CATS: [(MenuCategory, &'static str); 4] = [
const MENU_CATS: [(MenuCategory, &'static str); 5] = [
(MenuCategory::File, "File"),
(MenuCategory::Edit, "Edit"),
(MenuCategory::Render, "Render"),
(MenuCategory::Mode, "Mode"),
(MenuCategory::View, "View"),
];
@ -497,6 +513,8 @@ pub struct EditorState {
pub promote_snapshot_pushed: bool,
pub resize_drag: Option<ResizeDragState>,
pub active_free: Option<FreeNodeId>,
pub layout_mode: LayoutMode,
pub snapping: bool,
}
#[derive(Debug, Clone)]
@ -620,9 +638,19 @@ impl EditorState {
promote_snapshot_pushed: false,
resize_drag: None,
active_free: None,
layout_mode: LayoutMode::Free,
snapping: false,
}
}
/// rounds a coordinate to the nearest 0.25-line increment.
fn snap_to_grid(&self, value: f32) -> f32 {
if !self.snapping { return value; }
let unit = self.font_size * 0.325;
if unit <= 0.0 { return value; }
(value / unit).round() * unit
}
/// applies the current cursor delta to any active promote drag.
pub fn tick_promote_drag(&mut self) -> bool {
let (node_id, layer, origin, size, dx, dy, was_escalated) = {
@ -644,8 +672,8 @@ impl EditorState {
if let Some(pd) = self.promote_drag.as_mut() { pd.escalated = true; }
let placement = FreePlacement {
layer,
x: origin.0 + dx,
y: origin.1 + dy,
x: self.snap_to_grid(origin.0 + dx),
y: self.snap_to_grid(origin.1 + dy),
w: size.0,
h: size.1,
};
@ -664,8 +692,8 @@ impl EditorState {
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 new_w = if axes.0 { self.snap_to_grid((start_size.0 + dx).max(60.0)) } else { start_size.0 };
let new_h = if axes.1 { self.snap_to_grid((start_size.1 + dy).max(40.0)) } else { start_size.1 };
let changed = self
.free_placements
.get(&node_id)
@ -752,6 +780,7 @@ impl EditorState {
/// 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; }
if !matches!(self.layout_mode, LayoutMode::Free) { return; }
let existing = self.free_placements.get(&node_id).copied();
let (origin, size, layer) = match existing {
Some(p) => ((p.x, p.y), (p.w, p.h), p.layer),
@ -3590,6 +3619,13 @@ impl EditorState {
Message::ResizeRelease => {
self.resize_drag = None;
}
Message::SetLayoutMode(mode) => {
self.layout_mode = mode;
self.snapping = !matches!(mode, LayoutMode::Free);
}
Message::ToggleSnapping => {
self.snapping = !self.snapping;
}
}
}
@ -4807,6 +4843,13 @@ impl EditorState {
sep(),
item("Evaluate", "Ctrl+E", Message::SmartEval),
],
MenuCategory::Mode => vec![
item("Free", if matches!(self.layout_mode, LayoutMode::Free) { "" } else { "" }, Message::SetLayoutMode(LayoutMode::Free)),
item("Relative", if matches!(self.layout_mode, LayoutMode::Relative) { "" } else { "" }, Message::SetLayoutMode(LayoutMode::Relative)),
item("Anchored", if matches!(self.layout_mode, LayoutMode::Anchored) { "" } else { "" }, Message::SetLayoutMode(LayoutMode::Anchored)),
sep(),
item("Snapping", if self.snapping { "" } else { "" }, Message::ToggleSnapping),
],
MenuCategory::View => vec![
item("Zoom In", "Ctrl+=", Message::ZoomIn),
item("Zoom Out", "Ctrl+-", Message::ZoomOut),
@ -4825,6 +4868,7 @@ impl EditorState {
MenuCategory::File => "Export as Rust Library".len(),
MenuCategory::Edit => "Insert Table".len(),
MenuCategory::Render => "Evaluate".len(),
MenuCategory::Mode => "Anchored".len(),
MenuCategory::View => "Reset Zoom".len(),
};
let max_hint_chars = 13_usize; // widest hint string in chars

View File

@ -450,6 +450,10 @@ pub extern "C" fn viewport_send_command(handle: *mut ViewportHandle, command: u3
12 => h.state.update(editor::Message::SetRenderMode(editor::RenderMode::Editor)),
13 => h.state.update(editor::Message::SetRenderMode(editor::RenderMode::View)),
16 => h.state.settings_open = !h.state.settings_open,
17 => h.state.update(editor::Message::SetLayoutMode(editor::LayoutMode::Free)),
18 => h.state.update(editor::Message::SetLayoutMode(editor::LayoutMode::Relative)),
19 => h.state.update(editor::Message::SetLayoutMode(editor::LayoutMode::Anchored)),
20 => h.state.update(editor::Message::ToggleSnapping),
_ => return,
};
h.needs_redraw = true;
@ -660,3 +664,27 @@ pub extern "C" fn viewport_render_mode(handle: *mut ViewportHandle) -> u32 {
editor::RenderMode::View => 2,
}
}
/// returns 0 for free, 1 for relative, 2 for anchored layout mode.
#[unsafe(no_mangle)]
pub extern "C" fn viewport_layout_mode(handle: *mut ViewportHandle) -> u32 {
let h = match unsafe { handle.as_mut() } {
Some(h) => h,
None => return 0,
};
match h.state.layout_mode {
editor::LayoutMode::Free => 0,
editor::LayoutMode::Relative => 1,
editor::LayoutMode::Anchored => 2,
}
}
/// returns true when 0.25-line snapping is on for drags and resizes.
#[unsafe(no_mangle)]
pub extern "C" fn viewport_snapping(handle: *mut ViewportHandle) -> bool {
let h = match unsafe { handle.as_mut() } {
Some(h) => h,
None => return false,
};
h.state.snapping
}