Fixed the View mode bugs I'd been putting off. Added build scripts.
This commit is contained in:
parent
9b2de378ef
commit
e8a7f655ec
|
|
@ -0,0 +1,83 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
||||
BUILD="$ROOT/build"
|
||||
APP="$BUILD/bin/Acord.app"
|
||||
CONTENTS="$APP/Contents"
|
||||
MACOS="$CONTENTS/MacOS"
|
||||
RESOURCES="$CONTENTS/Resources"
|
||||
|
||||
SDK=$(xcrun --show-sdk-path)
|
||||
|
||||
RUST_LIB="$ROOT/target/release"
|
||||
export MACOSX_DEPLOYMENT_TARGET=14.0
|
||||
export ZERO_AR_DATE=0
|
||||
echo "Building Rust workspace (release)..."
|
||||
cd "$ROOT" && cargo build --release -p acord-viewport
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "ERROR: Rust build failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$RUST_LIB/libacord_viewport.a" ]; then
|
||||
echo "ERROR: libacord_viewport.a not found at $RUST_LIB"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
RUST_FLAGS=(-import-objc-header "$ROOT/viewport/include/acord.h" -L "$RUST_LIB" -lacord_viewport)
|
||||
|
||||
# --- App icon from SVG via rsvg-convert ---
|
||||
SVG="$ROOT/assets/Acord.svg"
|
||||
if [ -f "$SVG" ]; then
|
||||
echo "Generating app icon..."
|
||||
ICONSET="$BUILD/AppIcon.iconset"
|
||||
mkdir -p "$ICONSET"
|
||||
for size in 16 32 64 128 256 512 1024; do
|
||||
rsvg-convert --width="$size" --height="$size" "$SVG" -o "$ICONSET/icon_${size}.png"
|
||||
done
|
||||
cp "$ICONSET/icon_16.png" "$ICONSET/icon_16x16.png"
|
||||
cp "$ICONSET/icon_32.png" "$ICONSET/icon_16x16@2x.png"
|
||||
cp "$ICONSET/icon_32.png" "$ICONSET/icon_32x32.png"
|
||||
cp "$ICONSET/icon_64.png" "$ICONSET/icon_32x32@2x.png"
|
||||
cp "$ICONSET/icon_128.png" "$ICONSET/icon_128x128.png"
|
||||
cp "$ICONSET/icon_256.png" "$ICONSET/icon_128x128@2x.png"
|
||||
cp "$ICONSET/icon_256.png" "$ICONSET/icon_256x256.png"
|
||||
cp "$ICONSET/icon_512.png" "$ICONSET/icon_256x256@2x.png"
|
||||
cp "$ICONSET/icon_512.png" "$ICONSET/icon_512x512.png"
|
||||
cp "$ICONSET/icon_1024.png" "$ICONSET/icon_512x512@2x.png"
|
||||
rm -f "$ICONSET"/icon_*.png.tmp "$ICONSET"/icon_16.png "$ICONSET"/icon_32.png "$ICONSET"/icon_64.png "$ICONSET"/icon_128.png "$ICONSET"/icon_256.png "$ICONSET"/icon_512.png "$ICONSET"/icon_1024.png
|
||||
iconutil -c icns "$ICONSET" -o "$BUILD/AppIcon.icns"
|
||||
rm -rf "$ICONSET"
|
||||
fi
|
||||
|
||||
# --- Bundle structure ---
|
||||
mkdir -p "$MACOS" "$RESOURCES"
|
||||
cp "$ROOT/Info.plist" "$CONTENTS/Info.plist"
|
||||
if [ -f "$BUILD/AppIcon.icns" ]; then
|
||||
cp "$BUILD/AppIcon.icns" "$RESOURCES/AppIcon.icns"
|
||||
fi
|
||||
|
||||
# --- Compile Swift ---
|
||||
echo "Compiling Swift (release)..."
|
||||
swiftc \
|
||||
-target arm64-apple-macosx14.0 \
|
||||
-sdk "$SDK" \
|
||||
"${RUST_FLAGS[@]}" \
|
||||
-framework Cocoa \
|
||||
-framework SwiftUI \
|
||||
-framework Metal \
|
||||
-framework MetalKit \
|
||||
-framework QuartzCore \
|
||||
-framework CoreGraphics \
|
||||
-framework CoreFoundation \
|
||||
-O \
|
||||
-o "$MACOS/Acord" \
|
||||
"$ROOT"/src/*.swift
|
||||
|
||||
# --- Code sign ---
|
||||
codesign --force --sign - "$APP"
|
||||
|
||||
echo "Built: $APP"
|
||||
|
||||
open /Users/pszsh/External/Repositories/Acord/build/bin/Acord.app
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Debug build — same wiring as build.sh but unoptimised, with -g, and
|
||||
# launched in the foreground so Rust panics print straight to this terminal
|
||||
# (the panic hook in viewport/src/lib.rs flushes stderr before SIGABRT).
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
||||
BUILD="$ROOT/build"
|
||||
APP="$BUILD/bin/Acord.app"
|
||||
CONTENTS="$APP/Contents"
|
||||
MACOS="$CONTENTS/MacOS"
|
||||
RESOURCES="$CONTENTS/Resources"
|
||||
|
||||
SDK=$(xcrun --show-sdk-path)
|
||||
RUST_LIB="$ROOT/target/debug"
|
||||
|
||||
export MACOSX_DEPLOYMENT_TARGET=14.0
|
||||
export ZERO_AR_DATE=0
|
||||
export RUST_BACKTRACE=1
|
||||
|
||||
echo "Building Rust workspace (debug)..."
|
||||
cd "$ROOT" && cargo build -p acord-viewport
|
||||
|
||||
if [ ! -f "$RUST_LIB/libacord_viewport.a" ]; then
|
||||
echo "ERROR: libacord_viewport.a not found at $RUST_LIB"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
RUST_FLAGS=(-import-objc-header "$ROOT/viewport/include/acord.h" -L "$RUST_LIB" -lacord_viewport)
|
||||
|
||||
# --- Bundle structure ---
|
||||
mkdir -p "$MACOS" "$RESOURCES"
|
||||
cp "$ROOT/Info.plist" "$CONTENTS/Info.plist"
|
||||
if [ -f "$BUILD/AppIcon.icns" ]; then
|
||||
cp "$BUILD/AppIcon.icns" "$RESOURCES/AppIcon.icns"
|
||||
fi
|
||||
|
||||
# --- Compile Swift (debug) ---
|
||||
echo "Compiling Swift (debug)..."
|
||||
swiftc \
|
||||
-target arm64-apple-macosx14.0 \
|
||||
-sdk "$SDK" \
|
||||
"${RUST_FLAGS[@]}" \
|
||||
-framework Cocoa \
|
||||
-framework SwiftUI \
|
||||
-framework Metal \
|
||||
-framework MetalKit \
|
||||
-framework QuartzCore \
|
||||
-framework CoreGraphics \
|
||||
-framework CoreFoundation \
|
||||
-Onone -g \
|
||||
-o "$MACOS/Acord" \
|
||||
"$ROOT"/src/*.swift
|
||||
|
||||
codesign --force --sign - "$APP"
|
||||
|
||||
# --- Kill existing, launch in foreground so stderr lands here ---
|
||||
pkill -f "Acord.app/Contents/MacOS/Acord" 2>/dev/null || true
|
||||
sleep 0.3
|
||||
|
||||
echo
|
||||
echo "Launching $MACOS/Acord — Rust panics will print below."
|
||||
echo "(Ctrl+C to exit, or quit Acord normally.)"
|
||||
echo "----------------------------------------------------------"
|
||||
exec "$MACOS/Acord"
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
||||
DEST="/Applications/Acord.app"
|
||||
|
||||
echo "Building release..."
|
||||
"$ROOT/build.sh"
|
||||
|
||||
# Kill running instance before replacing
|
||||
pkill -f "Acord.app/Contents/MacOS/Acord" 2>/dev/null || true
|
||||
sleep 0.5
|
||||
|
||||
echo "Installing to $DEST..."
|
||||
rm -rf "$DEST"
|
||||
cp -R "$ROOT/build/bin/Acord.app" "$DEST"
|
||||
|
||||
echo "Installed: $DEST"
|
||||
|
|
@ -69,6 +69,8 @@ class TitleBarView: NSView {
|
|||
label.lineBreakMode = .byTruncatingTail
|
||||
label.cell?.truncatesLastVisibleLine = true
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
label.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
||||
label.stringValue = "Untitled"
|
||||
|
||||
editor.font = .systemFont(ofSize: 13, weight: .semibold)
|
||||
|
|
@ -81,6 +83,8 @@ class TitleBarView: NSView {
|
|||
editor.focusRingType = .none
|
||||
editor.cell?.lineBreakMode = .byTruncatingTail
|
||||
editor.translatesAutoresizingMaskIntoConstraints = false
|
||||
editor.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
editor.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
||||
editor.isHidden = true
|
||||
editor.delegate = self
|
||||
|
||||
|
|
@ -94,7 +98,7 @@ class TitleBarView: NSView {
|
|||
|
||||
editor.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||
editor.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||
editor.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.5),
|
||||
editor.widthAnchor.constraint(lessThanOrEqualTo: widthAnchor, multiplier: 0.5),
|
||||
])
|
||||
|
||||
let dblClick = NSClickGestureRecognizer(target: self, action: #selector(handleDoubleClick(_:)))
|
||||
|
|
|
|||
|
|
@ -2517,7 +2517,48 @@ impl EditorState {
|
|||
});
|
||||
}
|
||||
|
||||
/// Whether `message` is safe to dispatch while the editor is in
|
||||
/// `RenderMode::View`. Allowlist: scroll, click/drag selection,
|
||||
/// find, copy, zoom, focus, navigation — anything that doesn't
|
||||
/// touch document content. Edit-shaped `text_widget::Action`s and
|
||||
/// every mutating top-level `Message` get dropped at the gate.
|
||||
fn message_is_view_safe(message: &Message) -> bool {
|
||||
match message {
|
||||
Message::SetRenderMode(_) => true,
|
||||
Message::FocusBlock(_) => true,
|
||||
Message::TogglePreview => true,
|
||||
Message::MarkdownLink(_) => true,
|
||||
Message::ZoomIn | Message::ZoomOut | Message::ZoomReset => true,
|
||||
Message::ToggleFind | Message::HideFind => true,
|
||||
Message::FindQueryChanged(_)
|
||||
| Message::FindNext
|
||||
| Message::FindPrev => true,
|
||||
Message::ReplaceQueryChanged(_) => true,
|
||||
Message::TableMoveUp
|
||||
| Message::TableMoveDown
|
||||
| Message::TableMoveLeft
|
||||
| Message::TableMoveRight => true,
|
||||
Message::SelectAllBlocks => true,
|
||||
Message::ShowContextMenu { .. } | Message::HideContextMenu => true,
|
||||
Message::CopyLiteral(_) | Message::CopyFocusedTableSelection => true,
|
||||
Message::InlineResultPress { .. } | Message::InlineResultRelease => true,
|
||||
Message::EditorAction(action) | Message::BlockAction(_, action) => {
|
||||
!action.is_edit()
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, message: Message) {
|
||||
// View mode: drop anything that would change the document. Mode
|
||||
// switches, focus, scroll, click/drag selection, find, copy,
|
||||
// navigation — all pass through. Allowlist; new mutating
|
||||
// messages should fall through to the default `false` arm and
|
||||
// get dropped.
|
||||
if self.render_mode == RenderMode::View && !Self::message_is_view_safe(&message) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Drop whole-document selection on any message that isn't itself an
|
||||
// operation on that selection. Click, key press, table action — all
|
||||
// collapse the doc-wide selection back to single-block / single-cell.
|
||||
|
|
@ -3533,6 +3574,7 @@ impl EditorState {
|
|||
col_items.push(status_bar.into());
|
||||
|
||||
iced_widget::column(col_items)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.into()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -228,24 +228,33 @@ pub fn render(handle: &mut ViewportHandle) {
|
|||
new_cmd_a_armed = Some(false);
|
||||
}
|
||||
|
||||
// View mode: consume all events except mode-switch keys.
|
||||
// Ctrl+I, Ctrl+/, Ctrl+Esc, `i`, `/` are handled by their own
|
||||
// match arms below. Everything else is swallowed.
|
||||
// View mode: drop the keys that would write to the document so
|
||||
// the iced widget never sees them. `i` and `/` (mode-switch)
|
||||
// fall through to their own match arms below; mouse events,
|
||||
// scroll, navigation/selection keys, and modifier-prefixed
|
||||
// shortcuts also pass through and get message-layer gated.
|
||||
if handle.state.render_mode == RenderMode::View {
|
||||
let is_mode_switch = match event {
|
||||
let is_typing_char = match event {
|
||||
Event::Keyboard(keyboard::Event::KeyPressed {
|
||||
key: keyboard::Key::Character(c), modifiers, ..
|
||||
}) => {
|
||||
(modifiers.control() && (c.as_str() == "i" || c.as_str() == "/"))
|
||||
|| (!modifiers.logo() && !modifiers.control() && !modifiers.alt()
|
||||
&& (c.as_str() == "i" || c.as_str() == "/"))
|
||||
}) if !modifiers.logo() && !modifiers.control() && !modifiers.alt() => {
|
||||
c.as_str() != "i" && c.as_str() != "/"
|
||||
}
|
||||
Event::Keyboard(keyboard::Event::KeyPressed {
|
||||
key: keyboard::Key::Named(keyboard::key::Named::Escape), modifiers, ..
|
||||
}) => modifiers.control(),
|
||||
_ => false,
|
||||
};
|
||||
if !is_mode_switch {
|
||||
let is_destructive_named = matches!(
|
||||
event,
|
||||
Event::Keyboard(keyboard::Event::KeyPressed {
|
||||
key: keyboard::Key::Named(
|
||||
keyboard::key::Named::Backspace
|
||||
| keyboard::key::Named::Delete
|
||||
| keyboard::key::Named::Enter
|
||||
| keyboard::key::Named::Tab
|
||||
),
|
||||
..
|
||||
})
|
||||
);
|
||||
if is_typing_char || is_destructive_named {
|
||||
consumed.push(ev_idx);
|
||||
continue;
|
||||
}
|
||||
|
|
@ -574,8 +583,8 @@ pub fn render(handle: &mut ViewportHandle) {
|
|||
messages.push(Message::ExitCellEdit);
|
||||
consumed.push(ev_idx);
|
||||
} else {
|
||||
// Nothing to dismiss — chain mode switch.
|
||||
// Live → Editor, Editor → View
|
||||
// Nothing to dismiss — cycle modes:
|
||||
// Live → Editor → View → Live.
|
||||
match handle.state.render_mode {
|
||||
RenderMode::Live => {
|
||||
messages.push(Message::SetRenderMode(RenderMode::Editor));
|
||||
|
|
@ -585,7 +594,10 @@ pub fn render(handle: &mut ViewportHandle) {
|
|||
messages.push(Message::SetRenderMode(RenderMode::View));
|
||||
consumed.push(ev_idx);
|
||||
}
|
||||
RenderMode::View => {}
|
||||
RenderMode::View => {
|
||||
messages.push(Message::SetRenderMode(RenderMode::Live));
|
||||
consumed.push(ev_idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,27 +80,32 @@ pub struct AnchoredItem<'a, Message, Theme = iced_wgpu::core::Theme> {
|
|||
pub struct LineMetric {
|
||||
/// Widget-y of this line's first visual row (relative to text_bounds.y).
|
||||
pub widget_y: f32,
|
||||
/// Cosmic-buffer y of this line's first visual row. Buffer y advances
|
||||
/// by line_h per visual row (wrapped lines occupy multiple rows).
|
||||
pub buffer_y: f32,
|
||||
/// Cosmic-text's viewport-relative y of this line's first visual row —
|
||||
/// matches the y produced by `Selection::Caret(position).y` and what
|
||||
/// `Action::Click { y }` consumes (already scroll-adjusted, items
|
||||
/// invisible to it). Diverges from `widget_y` whenever an anchored
|
||||
/// item sits between this line and `scroll.line`.
|
||||
pub viewport_y: f32,
|
||||
/// Number of visual rows this logical line occupies after wrap.
|
||||
pub visual_rows: usize,
|
||||
}
|
||||
|
||||
/// Translate a cosmic-buffer y (visual rows * line_h) into a widget y.
|
||||
fn buffer_y_to_widget_y(metrics: &[LineMetric], buffer_y: f32) -> f32 {
|
||||
if metrics.is_empty() { return buffer_y; }
|
||||
/// Translate a cosmic-reported y (`Selection::Caret`, `Selection::Range`)
|
||||
/// into our widget-y so cursor + selection rectangles draw on top of the
|
||||
/// text rows the compositor actually rendered.
|
||||
fn cosmic_y_to_widget_y(metrics: &[LineMetric], cosmic_y: f32, _line_h: f32) -> f32 {
|
||||
if metrics.is_empty() { return cosmic_y; }
|
||||
for i in (0..metrics.len() - 1).rev() {
|
||||
if metrics[i].buffer_y <= buffer_y {
|
||||
return metrics[i].widget_y + (buffer_y - metrics[i].buffer_y);
|
||||
if metrics[i].viewport_y <= cosmic_y {
|
||||
return metrics[i].widget_y + (cosmic_y - metrics[i].viewport_y);
|
||||
}
|
||||
}
|
||||
metrics[0].widget_y + (buffer_y - metrics[0].buffer_y)
|
||||
metrics[0].widget_y + (cosmic_y - metrics[0].viewport_y)
|
||||
}
|
||||
|
||||
/// Translate a widget y into a cosmic-buffer y. Click/drag positions go
|
||||
/// through this so cosmic-text receives the right visual row.
|
||||
fn widget_y_to_buffer_y(metrics: &[LineMetric], widget_y: f32, line_h: f32) -> f32 {
|
||||
/// Translate a widget-y (mouse coords) back into the y cosmic-text expects
|
||||
/// for click/drag actions — the inverse of `cosmic_y_to_widget_y`.
|
||||
fn widget_y_to_cosmic_y(metrics: &[LineMetric], widget_y: f32, line_h: f32) -> f32 {
|
||||
if metrics.len() < 2 { return widget_y; }
|
||||
let line_count = metrics.len() - 1;
|
||||
for i in 0..line_count {
|
||||
|
|
@ -108,17 +113,17 @@ fn widget_y_to_buffer_y(metrics: &[LineMetric], widget_y: f32, line_h: f32) -> f
|
|||
let line_bot = line_top + metrics[i].visual_rows as f32 * line_h;
|
||||
if widget_y < line_bot {
|
||||
if widget_y < line_top {
|
||||
return metrics[i].buffer_y;
|
||||
return metrics[i].viewport_y;
|
||||
}
|
||||
return metrics[i].buffer_y + (widget_y - line_top);
|
||||
return metrics[i].viewport_y + (widget_y - line_top);
|
||||
}
|
||||
let next_top = metrics[i + 1].widget_y;
|
||||
if widget_y < next_top {
|
||||
return metrics[i].buffer_y + metrics[i].visual_rows as f32 * line_h;
|
||||
return metrics[i].viewport_y + metrics[i].visual_rows as f32 * line_h;
|
||||
}
|
||||
}
|
||||
let tail = metrics.last().unwrap();
|
||||
tail.buffer_y + (widget_y - tail.widget_y).max(0.0)
|
||||
tail.viewport_y + (widget_y - tail.widget_y).max(0.0)
|
||||
}
|
||||
|
||||
/// Distance-driven fade ratio for the gutter rainbow. `0.0` at the cursor
|
||||
|
|
@ -674,7 +679,7 @@ where
|
|||
|
||||
let adjusted = {
|
||||
let metrics = state.line_metrics.borrow();
|
||||
Point::new(cursor.x, buffer_y_to_widget_y(&metrics, cursor.y))
|
||||
Point::new(cursor.x, cosmic_y_to_widget_y(&metrics, cursor.y, line_height.into()))
|
||||
};
|
||||
|
||||
let position = adjusted + translation;
|
||||
|
|
@ -1001,30 +1006,42 @@ where
|
|||
// positions. Without this seeding, draw renders text at unscrolled
|
||||
// y while the cursor (computed via cosmic's scroll-aware selection)
|
||||
// appears to drift — the classic "two sources of truth" violation.
|
||||
// Anchor cosmic's viewport-y at scroll.line top: cosmic's
|
||||
// `Selection::Caret(position).y` for a cursor sitting on the very
|
||||
// first visible visual row equals `-scroll.vertical`, regardless
|
||||
// of how many logical lines came before. Pre-scroll lines are not
|
||||
// shaped (`layout_opt() == None`) and contribute 0 visual rows in
|
||||
// cosmic's own bookkeeping — mirror that so the two y-spaces
|
||||
// agree.
|
||||
let scroll = buffer.scroll();
|
||||
let mut scroll_offset_px: f32 = scroll.vertical;
|
||||
for i in 0..scroll.line.min(line_count) {
|
||||
let visual_rows = buffer.lines[i]
|
||||
.layout_opt()
|
||||
.map(|v| v.len())
|
||||
.unwrap_or(1)
|
||||
.max(1);
|
||||
scroll_offset_px += visual_rows as f32 * line_h;
|
||||
}
|
||||
let mut metrics: Vec<LineMetric> = Vec::with_capacity(line_count + 1);
|
||||
let mut widget_y = -scroll_offset_px;
|
||||
let mut buffer_y = 0.0f32;
|
||||
let mut widget_y = -scroll.vertical;
|
||||
let mut viewport_y = -scroll.vertical;
|
||||
// Pre-scroll lines: cosmic treats them as 0 rows, but we still
|
||||
// need a widget-y so any bottom-anchored items render relative to
|
||||
// a stable bottom. For the cursor/selection mapping to work the
|
||||
// viewport_y must stay parked at -scroll.vertical for them
|
||||
// (cosmic has no addressable y above scroll.line).
|
||||
for _ in 0..scroll.line.min(line_count) {
|
||||
metrics.push(LineMetric { widget_y, viewport_y, visual_rows: 0 });
|
||||
}
|
||||
let mut next_child = 0;
|
||||
for line in 0..line_count {
|
||||
// Skip anchored children that sit above the scroll line.
|
||||
while next_child < self.anchored_children.len()
|
||||
&& self.anchored_children[next_child].after_line < scroll.line
|
||||
{
|
||||
next_child += 1;
|
||||
}
|
||||
for line in scroll.line..line_count {
|
||||
let visual_rows = buffer.lines[line]
|
||||
.layout_opt()
|
||||
.map(|v| v.len())
|
||||
.unwrap_or(1)
|
||||
.max(1);
|
||||
metrics.push(LineMetric { widget_y, buffer_y, visual_rows });
|
||||
metrics.push(LineMetric { widget_y, viewport_y, visual_rows });
|
||||
let line_visual_h = visual_rows as f32 * line_h;
|
||||
widget_y += line_visual_h;
|
||||
buffer_y += line_visual_h;
|
||||
viewport_y += line_visual_h;
|
||||
while next_child < self.anchored_children.len()
|
||||
&& self.anchored_children[next_child].after_line == line
|
||||
{
|
||||
|
|
@ -1063,8 +1080,8 @@ where
|
|||
}
|
||||
// Push sentinel AFTER trailing children are placed, so the
|
||||
// sentinel widget_y reflects the true bottom of the stream.
|
||||
metrics.push(LineMetric { widget_y, buffer_y, visual_rows: 0 });
|
||||
let extra = widget_y - buffer_y;
|
||||
metrics.push(LineMetric { widget_y, viewport_y, visual_rows: 0 });
|
||||
let extra = widget_y - viewport_y;
|
||||
*state.line_metrics.borrow_mut() = metrics;
|
||||
|
||||
match self.height {
|
||||
|
|
@ -1185,7 +1202,7 @@ where
|
|||
let mut pos = click.position();
|
||||
pos.x = (pos.x - gw).max(0.0);
|
||||
let metrics = state.line_metrics.borrow();
|
||||
pos.y = widget_y_to_buffer_y(&metrics, pos.y, line_h);
|
||||
pos.y = widget_y_to_cosmic_y(&metrics, pos.y, line_h);
|
||||
Action::Click(pos)
|
||||
}
|
||||
mouse::click::Kind::Double => Action::SelectWord,
|
||||
|
|
@ -1204,7 +1221,7 @@ where
|
|||
let mut pos = position;
|
||||
pos.x = (pos.x - gw).max(0.0);
|
||||
let metrics = state.line_metrics.borrow();
|
||||
pos.y = widget_y_to_buffer_y(&metrics, pos.y, line_h);
|
||||
pos.y = widget_y_to_cosmic_y(&metrics, pos.y, line_h);
|
||||
shell.publish(on_edit(Action::Drag(pos)));
|
||||
}
|
||||
Update::Release => {
|
||||
|
|
@ -1527,6 +1544,12 @@ where
|
|||
Some(m) => m,
|
||||
None => continue,
|
||||
};
|
||||
// Pre-scroll lines carry visual_rows == 0 (cosmic hasn't
|
||||
// shaped them, layout_opt returns None) — skip them so
|
||||
// we don't draw unshaped paragraphs piled at the same y.
|
||||
if m.visual_rows == 0 {
|
||||
continue;
|
||||
}
|
||||
let y = text_bounds.y + m.widget_y;
|
||||
let row_h = m.visual_rows as f32 * line_h;
|
||||
|
||||
|
|
@ -1604,7 +1627,7 @@ where
|
|||
if let Some(focus) = state.focus.as_ref() {
|
||||
let metrics_for_cursor = state.line_metrics.borrow();
|
||||
let adjust_y = |pos: Point| -> Point {
|
||||
Point::new(pos.x, buffer_y_to_widget_y(&metrics_for_cursor, pos.y))
|
||||
Point::new(pos.x, cosmic_y_to_widget_y(&metrics_for_cursor, pos.y, line_h))
|
||||
};
|
||||
|
||||
match internal.editor.selection() {
|
||||
|
|
|
|||
Loading…
Reference in New Issue