Lasagna hat. Uh... no, it was tables which got some sorting sorted out, fixed the gutter to be drawn with the rest of the compositor's sweep.
This commit is contained in:
parent
210978e8f2
commit
43c1698797
|
|
@ -14,10 +14,6 @@
|
||||||
<stop offset="0.897" style="stop-color: rgb(248, 142, 0);" id="stop7"/>
|
<stop offset="0.897" style="stop-color: rgb(248, 142, 0);" id="stop7"/>
|
||||||
<stop offset="1" style="stop-color: rgb(233, 0, 0); stop-opacity: 0.827;" id="stop8"/>
|
<stop offset="1" style="stop-color: rgb(233, 0, 0); stop-opacity: 0.827;" id="stop8"/>
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<linearGradient gradientUnits="userSpaceOnUse" x1="247.952" y1="1.068" x2="247.952" y2="489.103" id="gradient-2" gradientTransform="translate(-12.620192)">
|
|
||||||
<stop offset="0" style="stop-color: rgb(166, 0, 140)" id="stop15"/>
|
|
||||||
<stop offset="1" style="stop-color: rgb(4, 6, 239);" id="stop16"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient gradientUnits="userSpaceOnUse" x1="227.47" y1="250.679" x2="227.47" y2="545.37598" id="gradient-1" gradientTransform="matrix(0.531844,0.846845,-0.939658,0.590138,518.63561,-7.578286)">
|
<linearGradient gradientUnits="userSpaceOnUse" x1="227.47" y1="250.679" x2="227.47" y2="545.37598" id="gradient-1" gradientTransform="matrix(0.531844,0.846845,-0.939658,0.590138,518.63561,-7.578286)">
|
||||||
<stop offset="0" style="stop-color: rgb(249, 0, 106);" id="stop17"/>
|
<stop offset="0" style="stop-color: rgb(249, 0, 106);" id="stop17"/>
|
||||||
<stop offset="0.088" style="stop-color: rgb(86, 0, 154);" id="stop18"/>
|
<stop offset="0.088" style="stop-color: rgb(86, 0, 154);" id="stop18"/>
|
||||||
|
|
@ -30,9 +26,22 @@
|
||||||
<stop offset="0.827" style="stop-color: rgb(244, 182, 87);" id="stop25"/>
|
<stop offset="0.827" style="stop-color: rgb(244, 182, 87);" id="stop25"/>
|
||||||
<stop offset="1" style="stop-color: rgb(225, 72, 72);" id="stop26"/>
|
<stop offset="1" style="stop-color: rgb(225, 72, 72);" id="stop26"/>
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
|
<radialGradient gradientUnits="userSpaceOnUse" cx="176.311" cy="197.825" r="243.31" id="gradient-2" gradientTransform="matrix(-1.802258, 2.07656, -0.757245, -0.657219, 890.103713, -190.568589)">
|
||||||
|
<stop offset="0" style="stop-color: rgb(255, 7, 0);"/>
|
||||||
|
<stop offset="0.111" style="stop-color: rgb(255, 255, 255); stop-opacity: 0.64;"/>
|
||||||
|
<stop offset="0.215" style="stop-color: rgb(40, 163, 242);"/>
|
||||||
|
<stop offset="0.314" style="stop-color: rgb(255, 0, 19);"/>
|
||||||
|
<stop offset="0.328" style="stop-opacity: 0.46;"/>
|
||||||
|
<stop offset="0.516" style="stop-color: rgb(245, 224, 255); stop-opacity: 0.58;"/>
|
||||||
|
<stop offset="0.516" style="stop-color: rgb(27, 28, 202);"/>
|
||||||
|
<stop offset="0.715" style="stop-color: rgb(232, 223, 194);"/>
|
||||||
|
<stop offset="0.79" style="stop-color: rgb(213, 176, 164);"/>
|
||||||
|
<stop offset="0.92" style="stop-color: rgb(206, 104, 91); stop-opacity: 0.02;"/>
|
||||||
|
<stop offset="1" style="stop-color: rgb(221, 32, 27); stop-opacity: 0.54;"/>
|
||||||
|
</radialGradient>
|
||||||
</defs>
|
</defs>
|
||||||
<g id="g29" transform="translate(9.4781923,0.43200004)">
|
<g id="g29" transform="matrix(1, 0, 0, 1, 9.478192, 0.432)">
|
||||||
<rect style="mix-blend-mode:darken;fill:#031056;fill-rule:nonzero;stroke:url(#gradient-2);stroke-width:3px;paint-order:stroke" x="-7.9781923" y="1.068" width="486.61899" height="488.035" rx="79" ry="79" id="rect26"/>
|
<rect style="fill-rule: nonzero; stroke-linecap: round; stroke-linejoin: round; vector-effect: non-scaling-stroke; paint-order: fill; stroke: url("#gradient-2"); stroke-width: 33px; fill: rgb(37, 54, 219);" x="-7.9781923" y="1.068" width="486.61899" height="488.035" id="rect26" rx="102.547" ry="102.547"/>
|
||||||
<path style="stroke:#ffffff;stroke-width:1.06069" d="M 349.92887,207.47326 97.322642,39.308335 141.23747,366.24784" id="path26"/>
|
<path style="stroke:#ffffff;stroke-width:1.06069" d="M 349.92887,207.47326 97.322642,39.308335 141.23747,366.24784" id="path26"/>
|
||||||
<path d="M 119.56386,429.97631 94.617429,36.739247 357.01616,205.03927 Z M 96.495189,40.380102 139.09489,370.77245 353.7893,205.40516 Z" style="fill:url(#gradient-0);fill-rule:nonzero;stroke:#ffd500;stroke-width:7.42481px;stroke-linecap:round;stroke-linejoin:round;paint-order:stroke" id="path27"/>
|
<path d="M 119.56386,429.97631 94.617429,36.739247 357.01616,205.03927 Z M 96.495189,40.380102 139.09489,370.77245 353.7893,205.40516 Z" style="fill:url(#gradient-0);fill-rule:nonzero;stroke:#ffd500;stroke-width:7.42481px;stroke-linecap:round;stroke-linejoin:round;paint-order:stroke" id="path27"/>
|
||||||
<path d="M 127.57 310.87 L 115.83 331.32 L 90.01 327.31 L 68.65 364.54 L 85.39 384.36 L 73.95 404.31 L 1.63 314.6 L 13.04 294.71 Z M 70.86 324.34 L 28.94 317.34 L 56.28 349.75 Z M 163.16 219.47 L 148.38 233.85 Q 144.13 229.39 139.41 228.86 Q 134.68 228.34 129.88 231.58 Q 123.51 235.88 122.68 242.84 Q 121.86 249.8 128.82 260.12 Q 136.56 271.6 143.61 273.74 Q 150.66 275.87 157.16 271.49 Q 162.01 268.21 163.25 263.36 Q 164.48 258.51 161.21 250.91 L 179.89 242.46 Q 185.06 255.76 181.63 266.65 Q 178.19 277.53 165.74 285.93 Q 151.6 295.47 137.17 292.22 Q 122.73 288.96 112.09 273.17 Q 101.32 257.2 103.76 242.61 Q 106.2 228.03 120.59 218.32 Q 132.37 210.38 142.74 210.76 Q 153.12 211.14 163.16 219.47 Z M 212.59 220.86 Q 214.01 210.71 220.36 201.91 Q 226.7 193.11 236.57 189.4 Q 246.45 185.68 257.77 187.27 Q 275.25 189.73 284.82 202.65 Q 294.39 215.58 291.96 232.91 Q 289.5 250.39 276.6 260.3 Q 263.71 270.21 246.59 267.8 Q 236 266.31 227.06 260.17 Q 218.13 254.04 214.42 244.08 Q 210.72 234.13 212.59 220.86 Z M 233.32 224.89 Q 231.71 236.35 236.29 243.2 Q 240.88 250.06 248.85 251.18 Q 256.83 252.3 263.09 246.97 Q 269.35 241.64 270.98 230.03 Q 272.58 218.72 268.03 211.87 Q 263.48 205.02 255.5 203.89 Q 247.53 202.77 241.23 208.1 Q 234.93 213.43 233.32 224.89 Z M 316.78 294.13 L 299.46 283.01 L 341.49 217.55 L 357.57 227.88 L 351.6 237.19 Q 359.96 233.25 364.6 233.27 Q 369.24 233.29 373.44 235.98 Q 379.35 239.78 382.74 246.57 L 367.68 258.23 Q 365.13 252.58 361.37 250.17 Q 357.73 247.83 353.92 248.21 Q 350.1 248.59 345.29 252.9 Q 340.48 257.21 329.76 273.91 Z M 379.82 382.06 L 370.78 365.22 L 380.85 359.81 Q 372.72 358.77 366.78 354.6 Q 360.85 350.43 357.73 344.63 Q 351.39 332.82 356.38 319.29 Q 361.38 305.75 378.41 296.61 Q 395.84 287.25 409.3 290.58 Q 422.77 293.9 429.49 306.42 Q 435.66 317.91 430.61 331.43 L 464.68 313.13 L 474.42 331.26 Z M 389.58 314.47 Q 378.61 320.36 375.34 326.02 Q 370.6 334.22 374.82 342.1 Q 378.19 348.36 385.86 349.88 Q 393.54 351.41 404.13 345.73 Q 415.94 339.39 418.84 332.34 Q 421.75 325.29 418.18 318.65 Q 414.72 312.2 407.25 310.59 Q 399.78 308.99 389.58 314.47 Z" transform="matrix(0.999336, 0, 0, 1.125805, -4.66082, -66.978394)" style="paint-order: stroke; stroke: rgb(255, 0, 0); stroke-linecap: round; stroke-linejoin: round; stroke-width: 6.20419px; text-wrap-mode: nowrap;"/>
|
<path d="M 127.57 310.87 L 115.83 331.32 L 90.01 327.31 L 68.65 364.54 L 85.39 384.36 L 73.95 404.31 L 1.63 314.6 L 13.04 294.71 Z M 70.86 324.34 L 28.94 317.34 L 56.28 349.75 Z M 163.16 219.47 L 148.38 233.85 Q 144.13 229.39 139.41 228.86 Q 134.68 228.34 129.88 231.58 Q 123.51 235.88 122.68 242.84 Q 121.86 249.8 128.82 260.12 Q 136.56 271.6 143.61 273.74 Q 150.66 275.87 157.16 271.49 Q 162.01 268.21 163.25 263.36 Q 164.48 258.51 161.21 250.91 L 179.89 242.46 Q 185.06 255.76 181.63 266.65 Q 178.19 277.53 165.74 285.93 Q 151.6 295.47 137.17 292.22 Q 122.73 288.96 112.09 273.17 Q 101.32 257.2 103.76 242.61 Q 106.2 228.03 120.59 218.32 Q 132.37 210.38 142.74 210.76 Q 153.12 211.14 163.16 219.47 Z M 212.59 220.86 Q 214.01 210.71 220.36 201.91 Q 226.7 193.11 236.57 189.4 Q 246.45 185.68 257.77 187.27 Q 275.25 189.73 284.82 202.65 Q 294.39 215.58 291.96 232.91 Q 289.5 250.39 276.6 260.3 Q 263.71 270.21 246.59 267.8 Q 236 266.31 227.06 260.17 Q 218.13 254.04 214.42 244.08 Q 210.72 234.13 212.59 220.86 Z M 233.32 224.89 Q 231.71 236.35 236.29 243.2 Q 240.88 250.06 248.85 251.18 Q 256.83 252.3 263.09 246.97 Q 269.35 241.64 270.98 230.03 Q 272.58 218.72 268.03 211.87 Q 263.48 205.02 255.5 203.89 Q 247.53 202.77 241.23 208.1 Q 234.93 213.43 233.32 224.89 Z M 316.78 294.13 L 299.46 283.01 L 341.49 217.55 L 357.57 227.88 L 351.6 237.19 Q 359.96 233.25 364.6 233.27 Q 369.24 233.29 373.44 235.98 Q 379.35 239.78 382.74 246.57 L 367.68 258.23 Q 365.13 252.58 361.37 250.17 Q 357.73 247.83 353.92 248.21 Q 350.1 248.59 345.29 252.9 Q 340.48 257.21 329.76 273.91 Z M 379.82 382.06 L 370.78 365.22 L 380.85 359.81 Q 372.72 358.77 366.78 354.6 Q 360.85 350.43 357.73 344.63 Q 351.39 332.82 356.38 319.29 Q 361.38 305.75 378.41 296.61 Q 395.84 287.25 409.3 290.58 Q 422.77 293.9 429.49 306.42 Q 435.66 317.91 430.61 331.43 L 464.68 313.13 L 474.42 331.26 Z M 389.58 314.47 Q 378.61 320.36 375.34 326.02 Q 370.6 334.22 374.82 342.1 Q 378.19 348.36 385.86 349.88 Q 393.54 351.41 404.13 345.73 Q 415.94 339.39 418.84 332.34 Q 421.75 325.29 418.18 318.65 Q 414.72 312.2 407.25 310.59 Q 399.78 308.99 389.58 314.47 Z" transform="matrix(0.999336, 0, 0, 1.125805, -4.66082, -66.978394)" style="paint-order: stroke; stroke: rgb(255, 0, 0); stroke-linecap: round; stroke-linejoin: round; stroke-width: 6.20419px; text-wrap-mode: nowrap;"/>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 12 KiB |
|
|
@ -74,6 +74,23 @@ pub enum Message {
|
||||||
InsertTable,
|
InsertTable,
|
||||||
ToggleBold,
|
ToggleBold,
|
||||||
ToggleItalic,
|
ToggleItalic,
|
||||||
|
ToggleStrike,
|
||||||
|
ToggleUnderline,
|
||||||
|
ToggleBlockquote,
|
||||||
|
/// Wrap the selection in matching delimiters; if the selection is
|
||||||
|
/// already wrapped (markers immediately surround it, with or without
|
||||||
|
/// being included in the selection), unwrap it.
|
||||||
|
WrapWith(&'static str, &'static str),
|
||||||
|
/// Insert a paired `[]` / `{}` and place the cursor between them.
|
||||||
|
/// Only applied to `[` and `{`; quotes/parens deliberately do NOT pair
|
||||||
|
/// on type — use Cmd+"/'/9 to wrap a selection.
|
||||||
|
AutoPair(&'static str, &'static str),
|
||||||
|
/// Cmd+0: incremental scope exit. Each press closes the innermost
|
||||||
|
/// unclosed pair within the current block; once everything is closed,
|
||||||
|
/// jumps the cursor past the next outer scope's closing delimiter; once
|
||||||
|
/// fully at block scope, ensures a "newline sandwich" (cursor on a
|
||||||
|
/// blank line with one blank line of padding above and below).
|
||||||
|
FixUp,
|
||||||
Evaluate,
|
Evaluate,
|
||||||
SmartEval,
|
SmartEval,
|
||||||
ZoomIn,
|
ZoomIn,
|
||||||
|
|
@ -1803,31 +1820,255 @@ impl EditorState {
|
||||||
self.rebuild_modules();
|
self.rebuild_modules();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn toggle_wrap(&mut self, marker: &str) {
|
/// Wrap a selection in matching delimiters or unwrap an existing pair.
|
||||||
let mlen = marker.len();
|
/// Used by Cmd+B (`**`), Cmd+I (`*`), Cmd+~ (`~~`), Cmd+", etc.
|
||||||
match self.content().selection() {
|
///
|
||||||
Some(sel) if sel.starts_with(marker) && sel.ends_with(marker) && sel.len() >= mlen * 2 => {
|
/// Unwrap detection looks at characters IMMEDIATELY outside the
|
||||||
let inner = &sel[mlen..sel.len() - mlen];
|
/// selection, not just inside it — so the selection can be the inner
|
||||||
|
/// text (without markers) and Cmd+B still toggles off.
|
||||||
|
///
|
||||||
|
/// Star-marker parity rule: bold (`**`) unwraps when the surrounding
|
||||||
|
/// star count on each side is >= 2 AND even (2 → 0, 4 → 2, …).
|
||||||
|
/// Italic (`*`) unwraps when the count is odd (1, 3, 5 …). This
|
||||||
|
/// keeps `**bold**` + Cmd+I → wraps to `***bold***` (bold-italic),
|
||||||
|
/// not destructive; and `***both***` + Cmd+B → `*both*` (strips bold).
|
||||||
|
fn toggle_wrap(&mut self, open: &str, close: &str) {
|
||||||
|
let text = self.content().text();
|
||||||
|
let cursor = self.content().cursor();
|
||||||
|
let pos = byte_offset_for_cursor(&text, &cursor.position);
|
||||||
|
let (start, end) = match self.selection_byte_range(&text, pos) {
|
||||||
|
Some(range) => range,
|
||||||
|
None => {
|
||||||
|
// No selection: insert paired markers and park cursor between.
|
||||||
|
let s = format!("{open}{close}");
|
||||||
|
self.content_mut().perform(text_widget::Action::Edit(
|
||||||
|
text_widget::Edit::Paste(Arc::new(s)),
|
||||||
|
));
|
||||||
|
for _ in 0..close.chars().count() {
|
||||||
|
self.content_mut().perform(text_widget::Action::Move(Motion::Left));
|
||||||
|
}
|
||||||
|
self.reparse();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let selected = &text[start..end];
|
||||||
|
let before = &text[..start];
|
||||||
|
let after = &text[end..];
|
||||||
|
|
||||||
|
let star_marker = open.chars().all(|c| c == '*') && close == open;
|
||||||
|
if star_marker {
|
||||||
|
let mlen = open.len();
|
||||||
|
// Sym-strip when markers are inside the selection itself.
|
||||||
|
if selected.starts_with(open) && selected.ends_with(close) && selected.len() >= mlen * 2 {
|
||||||
|
let inner = &selected[mlen..selected.len() - mlen];
|
||||||
self.content_mut().perform(text_widget::Action::Edit(
|
self.content_mut().perform(text_widget::Action::Edit(
|
||||||
text_widget::Edit::Paste(Arc::new(inner.to_string())),
|
text_widget::Edit::Paste(Arc::new(inner.to_string())),
|
||||||
));
|
));
|
||||||
|
self.reparse();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
Some(sel) => {
|
let outer = count_trailing_char(before, '*').min(count_leading_char(after, '*'));
|
||||||
let wrapped = format!("{marker}{sel}{marker}");
|
let should_unwrap = match mlen {
|
||||||
|
2 => outer >= 2 && outer % 2 == 0, // bold
|
||||||
|
1 => outer >= 1 && outer % 2 == 1, // italic
|
||||||
|
_ => outer >= mlen,
|
||||||
|
};
|
||||||
|
if should_unwrap {
|
||||||
|
self.replace_range(start - mlen, end + mlen, selected);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Non-star markers: simple symmetric strip.
|
||||||
|
let olen = open.len();
|
||||||
|
let clen = close.len();
|
||||||
|
if selected.starts_with(open) && selected.ends_with(close) && selected.len() >= olen + clen {
|
||||||
|
let inner = &selected[olen..selected.len() - clen];
|
||||||
self.content_mut().perform(text_widget::Action::Edit(
|
self.content_mut().perform(text_widget::Action::Edit(
|
||||||
text_widget::Edit::Paste(Arc::new(wrapped)),
|
text_widget::Edit::Paste(Arc::new(inner.to_string())),
|
||||||
));
|
));
|
||||||
|
self.reparse();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
None => {
|
if before.ends_with(open) && after.starts_with(close) {
|
||||||
let empty = format!("{marker}{marker}");
|
self.replace_range(start - olen, end + clen, selected);
|
||||||
self.content_mut().perform(text_widget::Action::Edit(
|
return;
|
||||||
text_widget::Edit::Paste(Arc::new(empty)),
|
|
||||||
));
|
|
||||||
for _ in 0..mlen {
|
|
||||||
self.content_mut().perform(text_widget::Action::Move(Motion::Left));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Default: wrap.
|
||||||
|
let wrapped = format!("{open}{selected}{close}");
|
||||||
|
self.content_mut().perform(text_widget::Action::Edit(
|
||||||
|
text_widget::Edit::Paste(Arc::new(wrapped)),
|
||||||
|
));
|
||||||
|
self.reparse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replace a byte range in the current content with `replacement`. Used
|
||||||
|
/// by toggle_wrap's unwrap path so we can rewrite text that sits OUTSIDE
|
||||||
|
/// the selection (the surrounding markers).
|
||||||
|
fn replace_range(&mut self, start: usize, end: usize, replacement: &str) {
|
||||||
|
let text = self.content().text();
|
||||||
|
if start > end || end > text.len() { return; }
|
||||||
|
let mut new_text = String::with_capacity(text.len() - (end - start) + replacement.len());
|
||||||
|
new_text.push_str(&text[..start]);
|
||||||
|
new_text.push_str(replacement);
|
||||||
|
new_text.push_str(&text[end..]);
|
||||||
|
// Rebuild the content with the new text and place cursor at end of
|
||||||
|
// replacement so successive toggles continue to operate at the
|
||||||
|
// same logical spot.
|
||||||
|
let cursor_byte = start + replacement.len();
|
||||||
|
self.content_mut().perform(text_widget::Action::Move(Motion::DocumentStart));
|
||||||
|
self.content_mut().perform(text_widget::Action::Select(Motion::DocumentEnd));
|
||||||
|
self.content_mut().perform(text_widget::Action::Edit(
|
||||||
|
text_widget::Edit::Paste(Arc::new(new_text.clone())),
|
||||||
|
));
|
||||||
|
// Position cursor at byte offset cursor_byte by walking from start.
|
||||||
|
let target = line_col_for_byte(&new_text, cursor_byte);
|
||||||
|
self.content_mut().perform(text_widget::Action::Move(Motion::DocumentStart));
|
||||||
|
for _ in 0..target.0 {
|
||||||
|
self.content_mut().perform(text_widget::Action::Move(Motion::Down));
|
||||||
|
}
|
||||||
|
self.content_mut().perform(text_widget::Action::Move(Motion::Home));
|
||||||
|
for _ in 0..target.1 {
|
||||||
|
self.content_mut().perform(text_widget::Action::Move(Motion::Right));
|
||||||
|
}
|
||||||
|
self.reparse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute the byte range of the current selection (start, end) or None
|
||||||
|
/// when no selection is active.
|
||||||
|
fn selection_byte_range(&self, text: &str, _cursor_pos: usize) -> Option<(usize, usize)> {
|
||||||
|
let sel = self.content().selection()?;
|
||||||
|
// We need the start position; use cursor + selection length to
|
||||||
|
// bracket. Selection is the text between selection-start and cursor;
|
||||||
|
// search both directions in the buffer to find a unique location.
|
||||||
|
// For toggle_wrap's purposes, we use the cursor position as the END
|
||||||
|
// and walk back by sel.len() to find start. This is correct when
|
||||||
|
// selection extends backward from the cursor; otherwise we fall
|
||||||
|
// back to a forward search.
|
||||||
|
let cursor = self.content().cursor();
|
||||||
|
let cursor_byte = byte_offset_for_cursor(text, &cursor.position);
|
||||||
|
let len = sel.len();
|
||||||
|
// Try cursor at end of selection.
|
||||||
|
if cursor_byte >= len && &text[cursor_byte - len..cursor_byte] == sel.as_str() {
|
||||||
|
return Some((cursor_byte - len, cursor_byte));
|
||||||
|
}
|
||||||
|
// Try cursor at start of selection.
|
||||||
|
if cursor_byte + len <= text.len() && &text[cursor_byte..cursor_byte + len] == sel.as_str() {
|
||||||
|
return Some((cursor_byte, cursor_byte + len));
|
||||||
|
}
|
||||||
|
// Fall back to searching the doc.
|
||||||
|
text.find(sel.as_str()).map(|s| (s, s + len))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert paired delimiters at the cursor and place the caret between
|
||||||
|
/// them. Used for `[` → `[|]` and `{` → `{|}`. Quotes/parens are
|
||||||
|
/// deliberately NOT auto-paired.
|
||||||
|
fn auto_pair(&mut self, open: &str, close: &str) {
|
||||||
|
let combined = format!("{open}{close}");
|
||||||
|
self.content_mut().perform(text_widget::Action::Edit(
|
||||||
|
text_widget::Edit::Paste(Arc::new(combined)),
|
||||||
|
));
|
||||||
|
for _ in 0..close.chars().count() {
|
||||||
|
self.content_mut().perform(text_widget::Action::Move(Motion::Left));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Toggle blockquote prefix on the current line(s). With a selection
|
||||||
|
/// spanning multiple lines, prefix `> ` to each; if every line already
|
||||||
|
/// has `> `, strip it.
|
||||||
|
fn toggle_blockquote(&mut self) {
|
||||||
|
let text = self.content().text();
|
||||||
|
let cursor = self.content().cursor();
|
||||||
|
let lines: Vec<&str> = text.lines().collect();
|
||||||
|
let cur_line = cursor.position.line.min(lines.len().saturating_sub(1));
|
||||||
|
// Single-line toggle: simplest meaningful form.
|
||||||
|
if cur_line >= lines.len() { return; }
|
||||||
|
let line = lines[cur_line];
|
||||||
|
let mut new_lines: Vec<String> = lines.iter().map(|l| l.to_string()).collect();
|
||||||
|
if let Some(rest) = line.strip_prefix("> ") {
|
||||||
|
new_lines[cur_line] = rest.to_string();
|
||||||
|
} else {
|
||||||
|
new_lines[cur_line] = format!("> {line}");
|
||||||
|
}
|
||||||
|
let new_text = new_lines.join("\n");
|
||||||
|
self.content_mut().perform(text_widget::Action::Move(Motion::DocumentStart));
|
||||||
|
self.content_mut().perform(text_widget::Action::Select(Motion::DocumentEnd));
|
||||||
|
self.content_mut().perform(text_widget::Action::Edit(
|
||||||
|
text_widget::Edit::Paste(Arc::new(new_text)),
|
||||||
|
));
|
||||||
|
self.reparse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cmd+0 catch-all. See `Message::FixUp` for the spec.
|
||||||
|
fn fix_up(&mut self) {
|
||||||
|
let text = self.content().text();
|
||||||
|
let cursor = self.content().cursor();
|
||||||
|
let pos = byte_offset_for_cursor(&text, &cursor.position);
|
||||||
|
// 1. Innermost unclosed delimiter? Close it.
|
||||||
|
if let Some(close) = innermost_unclosed_delim(&text[..pos]) {
|
||||||
|
self.content_mut().perform(text_widget::Action::Edit(
|
||||||
|
text_widget::Edit::Paste(Arc::new(close.to_string())),
|
||||||
|
));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 2. Forward to the next outer scope's closing delimiter and step past it.
|
||||||
|
if let Some(jump_to) = next_closing_delim_after(&text, pos) {
|
||||||
|
let target = line_col_for_byte(&text, jump_to + 1);
|
||||||
|
self.content_mut().perform(text_widget::Action::Move(Motion::DocumentStart));
|
||||||
|
for _ in 0..target.0 {
|
||||||
|
self.content_mut().perform(text_widget::Action::Move(Motion::Down));
|
||||||
|
}
|
||||||
|
self.content_mut().perform(text_widget::Action::Move(Motion::Home));
|
||||||
|
for _ in 0..target.1 {
|
||||||
|
self.content_mut().perform(text_widget::Action::Move(Motion::Right));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 3. At block scope: ensure newline sandwich.
|
||||||
|
self.ensure_newline_sandwich();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move the cursor onto its own line with exactly one blank line of
|
||||||
|
/// padding above and below (3 newlines total around the caret), or up
|
||||||
|
/// to EOF on either side.
|
||||||
|
fn ensure_newline_sandwich(&mut self) {
|
||||||
|
let text = self.content().text();
|
||||||
|
let cursor = self.content().cursor();
|
||||||
|
let pos = byte_offset_for_cursor(&text, &cursor.position);
|
||||||
|
// Walk back: collapse trailing whitespace/newlines before pos to "\n\n".
|
||||||
|
let mut left = pos;
|
||||||
|
while left > 0 {
|
||||||
|
let c = text[..left].chars().rev().next().unwrap();
|
||||||
|
if c == '\n' || c.is_whitespace() { left -= c.len_utf8(); } else { break; }
|
||||||
|
}
|
||||||
|
// Walk forward: collapse leading whitespace/newlines after pos to "\n\n".
|
||||||
|
let mut right = pos;
|
||||||
|
while right < text.len() {
|
||||||
|
let c = text[right..].chars().next().unwrap();
|
||||||
|
if c == '\n' || c.is_whitespace() { right += c.len_utf8(); } else { break; }
|
||||||
|
}
|
||||||
|
let prefix = if left == 0 { String::new() } else { "\n\n".to_string() };
|
||||||
|
let suffix = if right == text.len() { String::new() } else { "\n\n".to_string() };
|
||||||
|
let middle = "\n";
|
||||||
|
let new_text = format!("{}{}{}{}{}",
|
||||||
|
&text[..left], prefix, middle, suffix, &text[right..]);
|
||||||
|
let cursor_byte = left + prefix.len() + middle.len();
|
||||||
|
self.content_mut().perform(text_widget::Action::Move(Motion::DocumentStart));
|
||||||
|
self.content_mut().perform(text_widget::Action::Select(Motion::DocumentEnd));
|
||||||
|
self.content_mut().perform(text_widget::Action::Edit(
|
||||||
|
text_widget::Edit::Paste(Arc::new(new_text.clone())),
|
||||||
|
));
|
||||||
|
let target = line_col_for_byte(&new_text, cursor_byte);
|
||||||
|
self.content_mut().perform(text_widget::Action::Move(Motion::DocumentStart));
|
||||||
|
for _ in 0..target.0 {
|
||||||
|
self.content_mut().perform(text_widget::Action::Move(Motion::Down));
|
||||||
|
}
|
||||||
|
self.content_mut().perform(text_widget::Action::Move(Motion::Home));
|
||||||
|
for _ in 0..target.1 {
|
||||||
|
self.content_mut().perform(text_widget::Action::Move(Motion::Right));
|
||||||
|
}
|
||||||
self.reparse();
|
self.reparse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2429,12 +2670,14 @@ impl EditorState {
|
||||||
// Intentionally NOT calling run_eval() — see eval_segment_range
|
// Intentionally NOT calling run_eval() — see eval_segment_range
|
||||||
// for the destruction-class bug this avoids.
|
// for the destruction-class bug this avoids.
|
||||||
}
|
}
|
||||||
Message::ToggleBold => {
|
Message::ToggleBold => self.toggle_wrap("**", "**"),
|
||||||
self.toggle_wrap("**");
|
Message::ToggleItalic => self.toggle_wrap("*", "*"),
|
||||||
}
|
Message::ToggleStrike => self.toggle_wrap("~~", "~~"),
|
||||||
Message::ToggleItalic => {
|
Message::ToggleUnderline => self.toggle_wrap("<u>", "</u>"),
|
||||||
self.toggle_wrap("*");
|
Message::WrapWith(open, close) => self.toggle_wrap(open, close),
|
||||||
}
|
Message::ToggleBlockquote => self.toggle_blockquote(),
|
||||||
|
Message::AutoPair(open, close) => self.auto_pair(open, close),
|
||||||
|
Message::FixUp => self.fix_up(),
|
||||||
Message::Evaluate => {
|
Message::Evaluate => {
|
||||||
self.run_eval();
|
self.run_eval();
|
||||||
}
|
}
|
||||||
|
|
@ -3276,6 +3519,8 @@ impl EditorState {
|
||||||
if single_text_block {
|
if single_text_block {
|
||||||
let is_focused = bi == self.focused_block;
|
let is_focused = bi == self.focused_block;
|
||||||
let cursor_line = tb.content.cursor().position.line;
|
let cursor_line = tb.content.cursor().position.line;
|
||||||
|
let text = tb.content.text();
|
||||||
|
let decors = compute_line_decors(&text);
|
||||||
|
|
||||||
let anchored_items = self.build_anchored_items(tb.id);
|
let anchored_items = self.build_anchored_items(tb.id);
|
||||||
let editor = text_widget::TextEditor::new(&tb.content)
|
let editor = text_widget::TextEditor::new(&tb.content)
|
||||||
|
|
@ -3288,10 +3533,17 @@ impl EditorState {
|
||||||
.wrapping(Wrapping::Word)
|
.wrapping(Wrapping::Word)
|
||||||
.key_binding(macos_key_binding)
|
.key_binding(macos_key_binding)
|
||||||
.anchored(anchored_items)
|
.anchored(anchored_items)
|
||||||
|
.show_gutter(true)
|
||||||
|
.gutter_offset(0)
|
||||||
|
.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| {
|
.style(|_theme, _status| {
|
||||||
let p = palette::current();
|
let p = palette::current();
|
||||||
text_widget::Style {
|
text_widget::Style {
|
||||||
background: Background::Color(Color::TRANSPARENT),
|
background: Background::Color(p.base),
|
||||||
border: Border::default(),
|
border: Border::default(),
|
||||||
placeholder: p.overlay0,
|
placeholder: p.overlay0,
|
||||||
value: p.text,
|
value: p.text,
|
||||||
|
|
@ -3313,52 +3565,7 @@ impl EditorState {
|
||||||
)
|
)
|
||||||
.into();
|
.into();
|
||||||
|
|
||||||
let cursorline: Element<'_, Message, Theme, iced_wgpu::Renderer> =
|
block_elements.push(editor_el);
|
||||||
canvas::Canvas::new(Cursorline {
|
|
||||||
cursor_line: if is_focused { Some(cursor_line) } else { None },
|
|
||||||
font_size: self.font_size,
|
|
||||||
top_pad: title_bar_h,
|
|
||||||
item_offsets: self.item_offsets(tb.id),
|
|
||||||
indicator: self.line_indicator,
|
|
||||||
})
|
|
||||||
.width(Length::Fill)
|
|
||||||
.height(Length::Fill)
|
|
||||||
.into();
|
|
||||||
|
|
||||||
let editor_with_cursorline: Element<'_, Message, Theme, iced_wgpu::Renderer> =
|
|
||||||
iced_widget::stack![cursorline, editor_el]
|
|
||||||
.width(Length::Fill)
|
|
||||||
.height(Length::Fill)
|
|
||||||
.into();
|
|
||||||
|
|
||||||
let text = tb.content.text();
|
|
||||||
let line_count = tb.content.line_count();
|
|
||||||
let decors = compute_line_decors(&text);
|
|
||||||
let gutter = Gutter {
|
|
||||||
line_count,
|
|
||||||
global_line_offset: 0,
|
|
||||||
font_size: self.font_size,
|
|
||||||
scroll_offset: self.scroll_offset,
|
|
||||||
cursor_line: if is_focused { Some(cursor_line) } else { None },
|
|
||||||
top_pad: title_bar_h,
|
|
||||||
line_decors: decors,
|
|
||||||
item_offsets: self.item_offsets(tb.id),
|
|
||||||
indicator: self.line_indicator,
|
|
||||||
rainbow: self.gutter_rainbow,
|
|
||||||
};
|
|
||||||
let gw = gutter.gutter_width();
|
|
||||||
|
|
||||||
let gutter_canvas: Element<'_, Message, Theme, iced_wgpu::Renderer> =
|
|
||||||
canvas::Canvas::new(gutter)
|
|
||||||
.width(Length::Fixed(gw))
|
|
||||||
.height(Length::Fill)
|
|
||||||
.into();
|
|
||||||
|
|
||||||
block_elements.push(
|
|
||||||
iced_widget::row![gutter_canvas, editor_with_cursorline]
|
|
||||||
.height(Length::Fill)
|
|
||||||
.into()
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
let top_pad = if bi == 0 { title_bar_h } else { 0.0 };
|
let top_pad = if bi == 0 { title_bar_h } else { 0.0 };
|
||||||
let is_focused = bi == self.focused_block;
|
let is_focused = bi == self.focused_block;
|
||||||
|
|
@ -3366,6 +3573,13 @@ impl EditorState {
|
||||||
let anchored_items = self.build_anchored_items(tb.id);
|
let anchored_items = self.build_anchored_items(tb.id);
|
||||||
let items_h: f32 = anchored_items.iter().map(|a| a.height).sum();
|
let items_h: f32 = anchored_items.iter().map(|a| a.height).sum();
|
||||||
let editor_h = (actual_lines as f32) * line_h + top_pad + 8.0 + items_h;
|
let editor_h = (actual_lines as f32) * line_h + top_pad + 8.0 + items_h;
|
||||||
|
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 editor = text_widget::TextEditor::new(&tb.content)
|
let editor = text_widget::TextEditor::new(&tb.content)
|
||||||
.id(block_editor_id(tb.id))
|
.id(block_editor_id(tb.id))
|
||||||
.on_action(move |action| Message::BlockAction(block_idx, action))
|
.on_action(move |action| Message::BlockAction(block_idx, action))
|
||||||
|
|
@ -3376,10 +3590,17 @@ impl EditorState {
|
||||||
.wrapping(Wrapping::Word)
|
.wrapping(Wrapping::Word)
|
||||||
.key_binding(macos_key_binding)
|
.key_binding(macos_key_binding)
|
||||||
.anchored(anchored_items)
|
.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| {
|
.style(|_theme, _status| {
|
||||||
let p = palette::current();
|
let p = palette::current();
|
||||||
text_widget::Style {
|
text_widget::Style {
|
||||||
background: Background::Color(Color::TRANSPARENT),
|
background: Background::Color(p.base),
|
||||||
border: Border::default(),
|
border: Border::default(),
|
||||||
placeholder: p.overlay0,
|
placeholder: p.overlay0,
|
||||||
value: p.text,
|
value: p.text,
|
||||||
|
|
@ -3401,58 +3622,7 @@ impl EditorState {
|
||||||
)
|
)
|
||||||
.into();
|
.into();
|
||||||
|
|
||||||
let line_count = tb.content.line_count();
|
block_elements.push(editor_el);
|
||||||
let cursor_line = tb.content.cursor().position.line;
|
|
||||||
let text = tb.content.text();
|
|
||||||
let decors = compute_line_decors(&text);
|
|
||||||
let gutter = Gutter {
|
|
||||||
line_count,
|
|
||||||
global_line_offset: global_line,
|
|
||||||
font_size: self.font_size,
|
|
||||||
scroll_offset: 0.0,
|
|
||||||
cursor_line: if is_focused { Some(cursor_line) } else { None },
|
|
||||||
top_pad,
|
|
||||||
line_decors: decors,
|
|
||||||
item_offsets: self.item_offsets(tb.id),
|
|
||||||
indicator: self.line_indicator,
|
|
||||||
rainbow: self.gutter_rainbow,
|
|
||||||
};
|
|
||||||
global_line += line_count;
|
|
||||||
let gw = gutter.gutter_width();
|
|
||||||
|
|
||||||
let cursorline: Element<'_, Message, Theme, iced_wgpu::Renderer> =
|
|
||||||
canvas::Canvas::new(Cursorline {
|
|
||||||
cursor_line: if is_focused { Some(cursor_line) } else { None },
|
|
||||||
font_size: self.font_size,
|
|
||||||
top_pad,
|
|
||||||
item_offsets: self.item_offsets(tb.id),
|
|
||||||
indicator: self.line_indicator,
|
|
||||||
})
|
|
||||||
.width(Length::Fill)
|
|
||||||
.height(Length::Fixed(editor_h))
|
|
||||||
.into();
|
|
||||||
|
|
||||||
let editor_with_cursorline: Element<'_, Message, Theme, iced_wgpu::Renderer> =
|
|
||||||
iced_widget::stack![cursorline, editor_el]
|
|
||||||
.width(Length::Fill)
|
|
||||||
.height(Length::Fixed(editor_h))
|
|
||||||
.into();
|
|
||||||
|
|
||||||
let gutter_canvas: Element<'_, Message, Theme, iced_wgpu::Renderer> =
|
|
||||||
canvas::Canvas::new(gutter)
|
|
||||||
.width(Length::Fixed(gw))
|
|
||||||
.height(Length::Fixed(editor_h))
|
|
||||||
.into();
|
|
||||||
|
|
||||||
block_elements.push(
|
|
||||||
iced_widget::container(
|
|
||||||
iced_widget::row![gutter_canvas, editor_with_cursorline]
|
|
||||||
)
|
|
||||||
.width(Length::Fill)
|
|
||||||
.height(Length::Fixed(editor_h))
|
|
||||||
.into()
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -4281,8 +4451,12 @@ fn macos_key_binding(key_press: KeyPress) -> Option<Binding<Message>> {
|
||||||
keyboard::Key::Character("-") if modifiers.logo() => {
|
keyboard::Key::Character("-") if modifiers.logo() => {
|
||||||
Some(Binding::Custom(Message::ZoomOut))
|
Some(Binding::Custom(Message::ZoomOut))
|
||||||
}
|
}
|
||||||
keyboard::Key::Character("0") if modifiers.logo() => {
|
// Cmd+0 lives in handle.rs now (FixUp); Cmd+Shift+0 resets zoom.
|
||||||
Some(Binding::Custom(Message::ZoomReset))
|
keyboard::Key::Character("[") if !modifiers.logo() && !modifiers.alt() && !modifiers.control() => {
|
||||||
|
Some(Binding::Custom(Message::AutoPair("[", "]")))
|
||||||
|
}
|
||||||
|
keyboard::Key::Character("{") if !modifiers.logo() && !modifiers.alt() && !modifiers.control() => {
|
||||||
|
Some(Binding::Custom(Message::AutoPair("{", "}")))
|
||||||
}
|
}
|
||||||
keyboard::Key::Named(key::Named::Backspace) if modifiers.alt() => {
|
keyboard::Key::Named(key::Named::Backspace) if modifiers.alt() => {
|
||||||
Some(Binding::Sequence(vec![
|
Some(Binding::Sequence(vec![
|
||||||
|
|
@ -4377,6 +4551,96 @@ fn leading_whitespace(line: &str) -> &str {
|
||||||
&line[..end]
|
&line[..end]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Count consecutive trailing occurrences of `c` at the end of `s`.
|
||||||
|
fn count_trailing_char(s: &str, c: char) -> usize {
|
||||||
|
s.chars().rev().take_while(|&x| x == c).count()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Count consecutive leading occurrences of `c` at the start of `s`.
|
||||||
|
fn count_leading_char(s: &str, c: char) -> usize {
|
||||||
|
s.chars().take_while(|&x| x == c).count()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert an iced `Position { line, column }` to a byte offset within
|
||||||
|
/// `text`. column is interpreted as char count (cosmic-text convention).
|
||||||
|
fn byte_offset_for_cursor(text: &str, pos: &text_widget::Position) -> usize {
|
||||||
|
let mut byte = 0usize;
|
||||||
|
let mut line_idx = 0usize;
|
||||||
|
for line in text.split_inclusive('\n') {
|
||||||
|
if line_idx == pos.line {
|
||||||
|
let col = pos.column;
|
||||||
|
for (i, _) in line.char_indices().take(col) {
|
||||||
|
byte += line.as_bytes()[i..i + 1].len();
|
||||||
|
}
|
||||||
|
// Walk col chars precisely.
|
||||||
|
let mut walked = 0usize;
|
||||||
|
for (ci, _) in line.char_indices() {
|
||||||
|
if walked == col { return byte.saturating_sub(line.len()) + ci; }
|
||||||
|
walked += 1;
|
||||||
|
}
|
||||||
|
// col >= line length: clamp to end of line content (before \n).
|
||||||
|
return byte + line.trim_end_matches('\n').len();
|
||||||
|
}
|
||||||
|
byte += line.len();
|
||||||
|
line_idx += 1;
|
||||||
|
}
|
||||||
|
text.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Inverse of `byte_offset_for_cursor`. Returns (line, column).
|
||||||
|
fn line_col_for_byte(text: &str, byte: usize) -> (usize, usize) {
|
||||||
|
let mut acc = 0usize;
|
||||||
|
let mut line_idx = 0usize;
|
||||||
|
for line in text.split_inclusive('\n') {
|
||||||
|
if byte < acc + line.len() {
|
||||||
|
let local = &line[..byte - acc];
|
||||||
|
return (line_idx, local.chars().count());
|
||||||
|
}
|
||||||
|
acc += line.len();
|
||||||
|
line_idx += 1;
|
||||||
|
}
|
||||||
|
let last_line = text.lines().count().saturating_sub(1);
|
||||||
|
(last_line, text.lines().last().map(|l| l.chars().count()).unwrap_or(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Walk `text` left-to-right tracking a delimiter stack. Return the
|
||||||
|
/// `close` char of the innermost still-open pair, or None if balanced.
|
||||||
|
/// Pairs tracked: `()`, `[]`, `{}`. (Quotes/HTML are intentionally out
|
||||||
|
/// of scope — too ambiguous in markdown.)
|
||||||
|
fn innermost_unclosed_delim(text: &str) -> Option<char> {
|
||||||
|
let mut stack: Vec<char> = Vec::new();
|
||||||
|
for c in text.chars() {
|
||||||
|
match c {
|
||||||
|
'(' => stack.push(')'),
|
||||||
|
'[' => stack.push(']'),
|
||||||
|
'{' => stack.push('}'),
|
||||||
|
')' | ']' | '}' => {
|
||||||
|
if stack.last() == Some(&c) { stack.pop(); }
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stack.last().copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find the byte offset of the next outer scope's CLOSING delimiter
|
||||||
|
/// after `pos`. Used by FixUp to step the cursor out one scope.
|
||||||
|
fn next_closing_delim_after(text: &str, pos: usize) -> Option<usize> {
|
||||||
|
let mut depth: i32 = 0;
|
||||||
|
let bytes = text.as_bytes();
|
||||||
|
for i in pos..bytes.len() {
|
||||||
|
match bytes[i] {
|
||||||
|
b'(' | b'[' | b'{' => depth += 1,
|
||||||
|
b')' | b']' | b'}' => {
|
||||||
|
if depth == 0 { return Some(i); }
|
||||||
|
depth -= 1;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
/// Parse a markdown image reference `` from a line. Returns
|
/// Parse a markdown image reference `` from a line. Returns
|
||||||
/// `(alt, src)` if found. Only matches if the `![` is the first
|
/// `(alt, src)` if found. Only matches if the `![` is the first
|
||||||
/// non-whitespace on the line (inline images inside text are not rendered
|
/// non-whitespace on the line (inline images inside text are not rendered
|
||||||
|
|
|
||||||
|
|
@ -297,6 +297,41 @@ pub fn render(handle: &mut ViewportHandle) {
|
||||||
messages.push(Message::ToggleItalic);
|
messages.push(Message::ToggleItalic);
|
||||||
consumed.push(ev_idx);
|
consumed.push(ev_idx);
|
||||||
}
|
}
|
||||||
|
"u" => {
|
||||||
|
messages.push(Message::ToggleUnderline);
|
||||||
|
consumed.push(ev_idx);
|
||||||
|
}
|
||||||
|
"x" if modifiers.shift() => {
|
||||||
|
// Cmd+Shift+X: strikethrough (Cmd+S is reserved for save)
|
||||||
|
messages.push(Message::ToggleStrike);
|
||||||
|
consumed.push(ev_idx);
|
||||||
|
}
|
||||||
|
"." if modifiers.shift() => {
|
||||||
|
// Cmd+> : blockquote prefix
|
||||||
|
messages.push(Message::ToggleBlockquote);
|
||||||
|
consumed.push(ev_idx);
|
||||||
|
}
|
||||||
|
"\"" | "'" => {
|
||||||
|
// Cmd+" / Cmd+' wrap selection in matching quotes.
|
||||||
|
let q: &'static str = if c.as_str() == "\"" { "\"" } else { "'" };
|
||||||
|
messages.push(Message::WrapWith(q, q));
|
||||||
|
consumed.push(ev_idx);
|
||||||
|
}
|
||||||
|
"9" | "(" => {
|
||||||
|
// Cmd+9 (or Cmd+Shift+9 = Cmd+( ) wraps in parens.
|
||||||
|
messages.push(Message::WrapWith("(", ")"));
|
||||||
|
consumed.push(ev_idx);
|
||||||
|
}
|
||||||
|
"0" if modifiers.shift() => {
|
||||||
|
// Cmd+Shift+0: reset zoom (moved off Cmd+0 to make
|
||||||
|
// room for the FixUp catch-all).
|
||||||
|
messages.push(Message::ZoomReset);
|
||||||
|
consumed.push(ev_idx);
|
||||||
|
}
|
||||||
|
"0" => {
|
||||||
|
messages.push(Message::FixUp);
|
||||||
|
consumed.push(ev_idx);
|
||||||
|
}
|
||||||
"e" => {
|
"e" => {
|
||||||
messages.push(Message::SmartEval);
|
messages.push(Message::SmartEval);
|
||||||
consumed.push(ev_idx);
|
consumed.push(ev_idx);
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,9 @@ pub enum TableMessage {
|
||||||
BeginColReorder(usize),
|
BeginColReorder(usize),
|
||||||
BeginRowReorder(usize),
|
BeginRowReorder(usize),
|
||||||
EndDrag,
|
EndDrag,
|
||||||
|
/// Click on a column-header sort arrow: cycles that column through
|
||||||
|
/// Neutral → Asc → Desc → Neutral and re-applies the composite sort.
|
||||||
|
CycleSort(usize),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Trait-implementing block for tables. Owns all the per-table mutable state
|
/// Trait-implementing block for tables. Owns all the per-table mutable state
|
||||||
|
|
@ -163,8 +166,15 @@ pub struct TableBlock {
|
||||||
pub drag_select_baseline: std::collections::HashSet<(usize, usize)>,
|
pub drag_select_baseline: std::collections::HashSet<(usize, usize)>,
|
||||||
pub last_cursor_x: f32,
|
pub last_cursor_x: f32,
|
||||||
pub last_cursor_y: f32,
|
pub last_cursor_y: f32,
|
||||||
|
/// Composite sort. Each entry is `(col_idx, dir)`. The first entry is
|
||||||
|
/// the dominant sort key; later entries break ties within groups of
|
||||||
|
/// equal dominant values. Empty = no sort active (visual neutral).
|
||||||
|
pub sort_priority: Vec<(usize, SortDir)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum SortDir { Asc, Desc }
|
||||||
|
|
||||||
impl TableBlock {
|
impl TableBlock {
|
||||||
pub fn new(id: BlockId, rows: Vec<Vec<String>>, start_line: usize) -> Self {
|
pub fn new(id: BlockId, rows: Vec<Vec<String>>, start_line: usize) -> Self {
|
||||||
Self::build(id, rows, start_line, false, None)
|
Self::build(id, rows, start_line, false, None)
|
||||||
|
|
@ -223,9 +233,53 @@ impl TableBlock {
|
||||||
drag_select_baseline: std::collections::HashSet::new(),
|
drag_select_baseline: std::collections::HashSet::new(),
|
||||||
last_cursor_x: 0.0,
|
last_cursor_x: 0.0,
|
||||||
last_cursor_y: 0.0,
|
last_cursor_y: 0.0,
|
||||||
|
sort_priority: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Cycle the sort state of `col`: Neutral → Asc → Desc → Neutral.
|
||||||
|
/// First click on a previously-neutral column appends it to the
|
||||||
|
/// END of the priority list (least dominant). Re-clicking advances
|
||||||
|
/// its direction in place; the third click removes it.
|
||||||
|
pub fn cycle_sort(&mut self, col: usize) {
|
||||||
|
if let Some(idx) = self.sort_priority.iter().position(|(c, _)| *c == col) {
|
||||||
|
match self.sort_priority[idx].1 {
|
||||||
|
SortDir::Asc => self.sort_priority[idx].1 = SortDir::Desc,
|
||||||
|
SortDir::Desc => { self.sort_priority.remove(idx); }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.sort_priority.push((col, SortDir::Asc));
|
||||||
|
}
|
||||||
|
self.apply_sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sort state for a column, if any. Used by the header chrome to pick
|
||||||
|
/// the arrow tint and the optional precedence badge.
|
||||||
|
pub fn sort_state_for(&self, col: usize) -> Option<(SortDir, usize)> {
|
||||||
|
self.sort_priority.iter().enumerate().find_map(|(i, (c, d))| {
|
||||||
|
if *c == col { Some((*d, i)) } else { None }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply the composite sort to the data rows (everything below row 0,
|
||||||
|
/// which is the header). Stable across equal keys so existing intra-
|
||||||
|
/// group order is preserved.
|
||||||
|
pub fn apply_sort(&mut self) {
|
||||||
|
if self.sort_priority.is_empty() || self.rows.len() <= 2 { return; }
|
||||||
|
let priority = self.sort_priority.clone();
|
||||||
|
let (_, tail) = self.rows.split_at_mut(1);
|
||||||
|
tail.sort_by(|a, b| {
|
||||||
|
for (col, dir) in &priority {
|
||||||
|
let av = a.get(*col).map(|s| s.as_str()).unwrap_or("");
|
||||||
|
let bv = b.get(*col).map(|s| s.as_str()).unwrap_or("");
|
||||||
|
let ord = compare_alphanumeric(av, bv);
|
||||||
|
let ord = if *dir == SortDir::Desc { ord.reverse() } else { ord };
|
||||||
|
if ord != std::cmp::Ordering::Equal { return ord; }
|
||||||
|
}
|
||||||
|
std::cmp::Ordering::Equal
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
pub fn col_count(&self) -> usize {
|
pub fn col_count(&self) -> usize {
|
||||||
self.col_widths.len()
|
self.col_widths.len()
|
||||||
}
|
}
|
||||||
|
|
@ -599,6 +653,12 @@ impl TableBlock {
|
||||||
start_y: self.last_cursor_y,
|
start_y: self.last_cursor_y,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
TableMessage::CycleSort(col) => {
|
||||||
|
if self.read_only || col >= self.col_widths.len() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.cycle_sort(col);
|
||||||
|
}
|
||||||
TableMessage::EndDrag => {
|
TableMessage::EndDrag => {
|
||||||
self.resize_drag = None;
|
self.resize_drag = None;
|
||||||
self.row_resize_drag = None;
|
self.row_resize_drag = None;
|
||||||
|
|
@ -1053,29 +1113,67 @@ where
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
let letter_container = container(
|
let sort_state = block.sort_state_for(ci);
|
||||||
text(letter)
|
let arrow_color = |active: bool| -> Color {
|
||||||
.size(chrome_font)
|
if active { p.text } else { Color { a: 0.25, ..p.overlay0 } }
|
||||||
|
};
|
||||||
|
let (up_active, down_active) = match sort_state {
|
||||||
|
Some((SortDir::Asc, _)) => (true, false),
|
||||||
|
Some((SortDir::Desc, _)) => (false, true),
|
||||||
|
None => (false, false),
|
||||||
|
};
|
||||||
|
let arrows: Element<'a, Message, Theme, iced_wgpu::Renderer> = if chrome_active {
|
||||||
|
let up_glyph = text("\u{25B2}")
|
||||||
|
.size(chrome_font * 0.7)
|
||||||
.font(EDITOR_FONT)
|
.font(EDITOR_FONT)
|
||||||
.color(oklab::lighten_for_size(p.overlay0, chrome_font))
|
.color(arrow_color(up_active));
|
||||||
)
|
let down_glyph = text("\u{25BC}")
|
||||||
.width(Length::Fixed(*w))
|
.size(chrome_font * 0.7)
|
||||||
.height(Length::Fixed(header_h))
|
.font(EDITOR_FONT)
|
||||||
.padding(Padding { top: 0.0, right: 0.0, bottom: 0.0, left: 6.0 })
|
.color(arrow_color(down_active));
|
||||||
.style(move |_theme: &Theme| container::Style {
|
let stack = iced_widget::row![up_glyph, down_glyph]
|
||||||
background: bg_color.map(Background::Color),
|
.spacing(2.0)
|
||||||
border: Border::default(),
|
.align_y(iced_wgpu::core::Alignment::Center);
|
||||||
text_color: None,
|
MouseArea::new(
|
||||||
shadow: Shadow::default(),
|
container(stack)
|
||||||
snap: false,
|
.padding(Padding { top: 0.0, right: 4.0, bottom: 0.0, left: 4.0 })
|
||||||
});
|
)
|
||||||
let letter_cell: Element<'a, Message, Theme, iced_wgpu::Renderer> = if chrome_active {
|
.on_press(on_msg(TableMessage::CycleSort(ci)))
|
||||||
MouseArea::new(letter_container)
|
|
||||||
.on_press(on_msg(TableMessage::BeginColReorder(ci)))
|
|
||||||
.into()
|
.into()
|
||||||
} else {
|
} else {
|
||||||
letter_container.into()
|
container(text("")).width(Length::Fixed(0.0)).into()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let letter_inner: Element<'a, Message, Theme, iced_wgpu::Renderer> = iced_widget::row![
|
||||||
|
MouseArea::new(
|
||||||
|
container(
|
||||||
|
text(letter)
|
||||||
|
.size(chrome_font)
|
||||||
|
.font(EDITOR_FONT)
|
||||||
|
.color(oklab::lighten_for_size(p.overlay0, chrome_font))
|
||||||
|
)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.padding(Padding { top: 0.0, right: 0.0, bottom: 0.0, left: 6.0 })
|
||||||
|
)
|
||||||
|
.on_press(on_msg(TableMessage::BeginColReorder(ci))),
|
||||||
|
arrows,
|
||||||
|
]
|
||||||
|
.spacing(0.0)
|
||||||
|
.align_y(iced_wgpu::core::Alignment::Center)
|
||||||
|
.into();
|
||||||
|
|
||||||
|
let letter_container = container(letter_inner)
|
||||||
|
.width(Length::Fixed(*w))
|
||||||
|
.height(Length::Fixed(header_h))
|
||||||
|
.style(move |_theme: &Theme| container::Style {
|
||||||
|
background: bg_color.map(Background::Color),
|
||||||
|
border: Border::default(),
|
||||||
|
text_color: None,
|
||||||
|
shadow: Shadow::default(),
|
||||||
|
snap: false,
|
||||||
|
});
|
||||||
|
let letter_cell: Element<'a, Message, Theme, iced_wgpu::Renderer> =
|
||||||
|
letter_container.into();
|
||||||
header_row_cells.push(letter_cell);
|
header_row_cells.push(letter_cell);
|
||||||
header_row_cells.push(
|
header_row_cells.push(
|
||||||
container(text(""))
|
container(text(""))
|
||||||
|
|
@ -1342,6 +1440,48 @@ fn column_letter(mut idx: usize) -> String {
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Natural alphanumeric comparison: contiguous digit runs compare as
|
||||||
|
/// integers so `R10` sorts after `R2`. Letter runs compare case-insensitive.
|
||||||
|
fn compare_alphanumeric(a: &str, b: &str) -> std::cmp::Ordering {
|
||||||
|
use std::cmp::Ordering;
|
||||||
|
let mut ai = a.chars().peekable();
|
||||||
|
let mut bi = b.chars().peekable();
|
||||||
|
loop {
|
||||||
|
match (ai.peek(), bi.peek()) {
|
||||||
|
(None, None) => return Ordering::Equal,
|
||||||
|
(None, Some(_)) => return Ordering::Less,
|
||||||
|
(Some(_), None) => return Ordering::Greater,
|
||||||
|
(Some(&ca), Some(&cb)) => {
|
||||||
|
if ca.is_ascii_digit() && cb.is_ascii_digit() {
|
||||||
|
let mut an = 0u64;
|
||||||
|
let mut bn = 0u64;
|
||||||
|
while let Some(&c) = ai.peek() {
|
||||||
|
if !c.is_ascii_digit() { break; }
|
||||||
|
an = an.saturating_mul(10) + (c as u64 - b'0' as u64);
|
||||||
|
ai.next();
|
||||||
|
}
|
||||||
|
while let Some(&c) = bi.peek() {
|
||||||
|
if !c.is_ascii_digit() { break; }
|
||||||
|
bn = bn.saturating_mul(10) + (c as u64 - b'0' as u64);
|
||||||
|
bi.next();
|
||||||
|
}
|
||||||
|
match an.cmp(&bn) {
|
||||||
|
Ordering::Equal => continue,
|
||||||
|
non_eq => return non_eq,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let la = ca.to_ascii_lowercase();
|
||||||
|
let lb = cb.to_ascii_lowercase();
|
||||||
|
match la.cmp(&lb) {
|
||||||
|
Ordering::Equal => { ai.next(); bi.next(); continue; }
|
||||||
|
non_eq => return non_eq,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn plus_button_style(_theme: &Theme, status: button::Status) -> button::Style {
|
fn plus_button_style(_theme: &Theme, status: button::Status) -> button::Style {
|
||||||
let p = palette::current();
|
let p = palette::current();
|
||||||
let ws = palette::widget_surface();
|
let ws = palette::widget_surface();
|
||||||
|
|
|
||||||
|
|
@ -74,42 +74,59 @@ pub struct AnchoredItem<'a, Message, Theme = iced_wgpu::core::Theme> {
|
||||||
pub element: Element<'a, Message, Theme, iced_wgpu::Renderer>,
|
pub element: Element<'a, Message, Theme, iced_wgpu::Renderer>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Walk the content stream (text lines + anchored items) and map widget-space y to text-space y.
|
/// Per-logical-line metrics. Stored on State so layout publishes once
|
||||||
fn stream_y_to_text_y<M, T>(y: f32, items: &[AnchoredItem<'_, M, T>], line_h: f32, line_count: usize) -> f32 {
|
/// and every consumer (draw, cursor, hit-test) reads the same data.
|
||||||
let mut text_y = 0.0f32;
|
#[derive(Clone, Default, Debug)]
|
||||||
let mut widget_y = 0.0f32;
|
pub struct LineMetric {
|
||||||
let mut item_idx = 0;
|
/// 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,
|
||||||
|
/// Number of visual rows this logical line occupies after wrap.
|
||||||
|
pub visual_rows: usize,
|
||||||
|
}
|
||||||
|
|
||||||
for line in 0..line_count {
|
/// Translate a cosmic-buffer y (visual rows * line_h) into a widget y.
|
||||||
if y < widget_y + line_h {
|
fn buffer_y_to_widget_y(metrics: &[LineMetric], buffer_y: f32) -> f32 {
|
||||||
return text_y + (y - widget_y);
|
if metrics.is_empty() { return buffer_y; }
|
||||||
}
|
for i in (0..metrics.len() - 1).rev() {
|
||||||
text_y += line_h;
|
if metrics[i].buffer_y <= buffer_y {
|
||||||
widget_y += line_h;
|
return metrics[i].widget_y + (buffer_y - metrics[i].buffer_y);
|
||||||
|
|
||||||
while item_idx < items.len() && items[item_idx].after_line == line {
|
|
||||||
let ih = items[item_idx].height;
|
|
||||||
if y < widget_y + ih {
|
|
||||||
return text_y;
|
|
||||||
}
|
|
||||||
widget_y += ih;
|
|
||||||
item_idx += 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
text_y + (y - widget_y).max(0.0)
|
metrics[0].widget_y + (buffer_y - metrics[0].buffer_y)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cumulative height of anchored items before a given text line.
|
/// Translate a widget y into a cosmic-buffer y. Click/drag positions go
|
||||||
fn items_height_before_line<M, T>(items: &[AnchoredItem<'_, M, T>], line: usize) -> f32 {
|
/// through this so cosmic-text receives the right visual row.
|
||||||
items.iter()
|
fn widget_y_to_buffer_y(metrics: &[LineMetric], widget_y: f32, line_h: f32) -> f32 {
|
||||||
.filter(|it| it.after_line < line)
|
if metrics.len() < 2 { return widget_y; }
|
||||||
.map(|it| it.height)
|
let line_count = metrics.len() - 1;
|
||||||
.sum()
|
for i in 0..line_count {
|
||||||
|
let line_top = metrics[i].widget_y;
|
||||||
|
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].buffer_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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let tail = metrics.last().unwrap();
|
||||||
|
tail.buffer_y + (widget_y - tail.widget_y).max(0.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Total height of all anchored items.
|
/// Distance-driven fade ratio for the gutter rainbow. `0.0` at the cursor
|
||||||
fn total_items_height<M, T>(items: &[AnchoredItem<'_, M, T>]) -> f32 {
|
/// (full saturation), `1.0` at the far end of the fade window.
|
||||||
items.iter().map(|it| it.height).sum()
|
const GUTTER_FADE_CYCLES: f32 = 2.5;
|
||||||
|
fn gutter_fade_t(distance: usize) -> f32 {
|
||||||
|
let max_d = GUTTER_FADE_CYCLES * crate::syntax::USER_IDENT_PALETTE_SIZE as f32;
|
||||||
|
(distance as f32 / max_d).min(1.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build iced Spans from a LayoutRun's glyphs, grouping consecutive glyphs by color.
|
/// Build iced Spans from a LayoutRun's glyphs, grouping consecutive glyphs by color.
|
||||||
|
|
@ -230,6 +247,11 @@ pub struct TextEditor<
|
||||||
anchored_children: Vec<AnchoredItem<'a, Message, Theme>>,
|
anchored_children: Vec<AnchoredItem<'a, Message, Theme>>,
|
||||||
gutter_offset: usize,
|
gutter_offset: usize,
|
||||||
is_focused_block: bool,
|
is_focused_block: bool,
|
||||||
|
show_gutter: bool,
|
||||||
|
cursor_line: Option<usize>,
|
||||||
|
line_indicator: crate::editor::LineIndicator,
|
||||||
|
gutter_rainbow: bool,
|
||||||
|
line_decors: Vec<crate::syntax::LineDecor>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, Message, Theme>
|
impl<'a, Message, Theme>
|
||||||
|
|
@ -263,6 +285,11 @@ where
|
||||||
anchored_children: Vec::new(),
|
anchored_children: Vec::new(),
|
||||||
gutter_offset: 0,
|
gutter_offset: 0,
|
||||||
is_focused_block: false,
|
is_focused_block: false,
|
||||||
|
show_gutter: false,
|
||||||
|
cursor_line: None,
|
||||||
|
line_indicator: crate::editor::LineIndicator::On,
|
||||||
|
gutter_rainbow: false,
|
||||||
|
line_decors: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -390,6 +417,11 @@ where
|
||||||
anchored_children: self.anchored_children,
|
anchored_children: self.anchored_children,
|
||||||
gutter_offset: self.gutter_offset,
|
gutter_offset: self.gutter_offset,
|
||||||
is_focused_block: self.is_focused_block,
|
is_focused_block: self.is_focused_block,
|
||||||
|
show_gutter: self.show_gutter,
|
||||||
|
cursor_line: self.cursor_line,
|
||||||
|
line_indicator: self.line_indicator,
|
||||||
|
gutter_rainbow: self.gutter_rainbow,
|
||||||
|
line_decors: self.line_decors,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -440,6 +472,170 @@ where
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reserve a left strip for line numbers + decoration stripes.
|
||||||
|
pub fn show_gutter(mut self, show: bool) -> Self {
|
||||||
|
self.show_gutter = show;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cursor's current line within this block. `None` when not focused.
|
||||||
|
/// Drives both the cursorline tint and the gutter rainbow center.
|
||||||
|
pub fn cursor_line(mut self, line: Option<usize>) -> Self {
|
||||||
|
self.cursor_line = line;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn line_indicator(mut self, ind: crate::editor::LineIndicator) -> Self {
|
||||||
|
self.line_indicator = ind;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn gutter_rainbow(mut self, on: bool) -> Self {
|
||||||
|
self.gutter_rainbow = on;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn line_decors(mut self, decors: Vec<crate::syntax::LineDecor>) -> Self {
|
||||||
|
self.line_decors = decors;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Width of the gutter strip given a line count. Caller passes the
|
||||||
|
/// count so this never touches `self.content` (which would deadlock
|
||||||
|
/// when called from inside layout/draw — those already hold the
|
||||||
|
/// content's RefCell).
|
||||||
|
fn gutter_width_for(&self, line_count: usize) -> f32 {
|
||||||
|
if !self.show_gutter { return 0.0; }
|
||||||
|
let total = self.gutter_offset + line_count;
|
||||||
|
let count = if total == 0 { 1 } else { total };
|
||||||
|
let digits = (count as f32).log10().floor() as usize + 1;
|
||||||
|
let font_size: f32 = self.text_size
|
||||||
|
.map(f32::from)
|
||||||
|
.unwrap_or(14.0);
|
||||||
|
let char_width = font_size * 0.6;
|
||||||
|
(digits.max(2) as f32 * char_width + 16.0).ceil()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_gutter_line(
|
||||||
|
&self,
|
||||||
|
renderer: &mut iced_wgpu::Renderer,
|
||||||
|
line_i: usize,
|
||||||
|
bounds: Rectangle,
|
||||||
|
y: f32,
|
||||||
|
line_h: f32,
|
||||||
|
gw: f32,
|
||||||
|
p: &crate::palette::Palette,
|
||||||
|
font_size: f32,
|
||||||
|
) {
|
||||||
|
use crate::syntax::LineDecor;
|
||||||
|
use crate::editor::LineIndicator;
|
||||||
|
|
||||||
|
let gutter_left = bounds.x + self.padding.left;
|
||||||
|
let gutter_right = gutter_left + gw;
|
||||||
|
|
||||||
|
let decor = self.line_decors.get(line_i).copied().unwrap_or(LineDecor::None);
|
||||||
|
match decor {
|
||||||
|
LineDecor::CodeBlock | LineDecor::FenceMarker => {
|
||||||
|
let bg = Color { a: 0.15, ..p.surface2 };
|
||||||
|
renderer.fill_quad(
|
||||||
|
renderer::Quad {
|
||||||
|
bounds: Rectangle::new(
|
||||||
|
Point::new(gutter_left, y),
|
||||||
|
Size::new(gw, line_h),
|
||||||
|
),
|
||||||
|
border: Border::default(),
|
||||||
|
..renderer::Quad::default()
|
||||||
|
},
|
||||||
|
Background::Color(bg),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
LineDecor::Blockquote => {
|
||||||
|
renderer.fill_quad(
|
||||||
|
renderer::Quad {
|
||||||
|
bounds: Rectangle::new(
|
||||||
|
Point::new(gutter_right - 3.0, y),
|
||||||
|
Size::new(3.0, line_h),
|
||||||
|
),
|
||||||
|
border: Border::default(),
|
||||||
|
..renderer::Quad::default()
|
||||||
|
},
|
||||||
|
Background::Color(p.lavender),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
LineDecor::HorizontalRule => {
|
||||||
|
let mid_y = y + line_h / 2.0;
|
||||||
|
let stroke_color = crate::oklab::lighten_for_size(p.overlay1, 1.0);
|
||||||
|
renderer.fill_quad(
|
||||||
|
renderer::Quad {
|
||||||
|
bounds: Rectangle::new(
|
||||||
|
Point::new(gutter_left + 4.0, mid_y - 0.5),
|
||||||
|
Size::new(gw - 8.0, 1.0),
|
||||||
|
),
|
||||||
|
border: Border::default(),
|
||||||
|
..renderer::Quad::default()
|
||||||
|
},
|
||||||
|
Background::Color(stroke_color),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
LineDecor::None => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.line_indicator == LineIndicator::Off {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let raw_color = if self.gutter_rainbow {
|
||||||
|
match self.cursor_line {
|
||||||
|
Some(cl) if line_i == cl => p.text,
|
||||||
|
Some(cl) if line_i > cl => {
|
||||||
|
let d = line_i - cl - 1;
|
||||||
|
let hue = crate::syntax::rainbow_color(d as u32);
|
||||||
|
crate::oklab::desaturate(hue, gutter_fade_t(d))
|
||||||
|
}
|
||||||
|
Some(cl) => {
|
||||||
|
let d = cl - line_i - 1;
|
||||||
|
let hue = crate::oklab::invert_hue(crate::syntax::rainbow_color(d as u32));
|
||||||
|
crate::oklab::desaturate(hue, gutter_fade_t(d))
|
||||||
|
}
|
||||||
|
None => p.surface2,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match self.cursor_line {
|
||||||
|
Some(cl) if line_i == cl => p.text,
|
||||||
|
_ => p.surface2,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let line_num = self.gutter_offset + line_i;
|
||||||
|
let label = match (self.line_indicator, self.cursor_line) {
|
||||||
|
(LineIndicator::Vim, Some(cl)) if line_i != cl => {
|
||||||
|
let d = if line_i > cl { line_i - cl } else { cl - line_i };
|
||||||
|
format!("{d}")
|
||||||
|
}
|
||||||
|
_ => format!("{}", line_num + 1),
|
||||||
|
};
|
||||||
|
|
||||||
|
renderer.fill_text(
|
||||||
|
Text {
|
||||||
|
content: label,
|
||||||
|
bounds: Size::new(gw, line_h),
|
||||||
|
size: Pixels(font_size),
|
||||||
|
line_height: self.line_height,
|
||||||
|
font: Font::MONOSPACE,
|
||||||
|
align_x: text::Alignment::Right,
|
||||||
|
align_y: alignment::Vertical::Top,
|
||||||
|
shaping: text::Shaping::Basic,
|
||||||
|
wrapping: Wrapping::None,
|
||||||
|
},
|
||||||
|
Point::new(gutter_right - 8.0, y),
|
||||||
|
crate::oklab::lighten_for_size(raw_color, font_size),
|
||||||
|
Rectangle::new(
|
||||||
|
Point::new(gutter_left, y),
|
||||||
|
Size::new(gw, line_h),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn input_method<'b>(
|
fn input_method<'b>(
|
||||||
&self,
|
&self,
|
||||||
state: &'b State<Highlighter>,
|
state: &'b State<Highlighter>,
|
||||||
|
|
@ -457,7 +653,12 @@ where
|
||||||
let bounds = layout.bounds();
|
let bounds = layout.bounds();
|
||||||
let internal = self.content.0.borrow_mut();
|
let internal = self.content.0.borrow_mut();
|
||||||
|
|
||||||
let text_bounds = bounds.shrink(self.padding);
|
let gw = state.gutter_width.get();
|
||||||
|
let effective_padding = Padding {
|
||||||
|
left: self.padding.left + gw,
|
||||||
|
..self.padding
|
||||||
|
};
|
||||||
|
let text_bounds = bounds.shrink(effective_padding);
|
||||||
let translation = text_bounds.position() - Point::ORIGIN;
|
let translation = text_bounds.position() - Point::ORIGIN;
|
||||||
|
|
||||||
let cursor = match internal.editor.selection() {
|
let cursor = match internal.editor.selection() {
|
||||||
|
|
@ -471,13 +672,9 @@ where
|
||||||
self.text_size.unwrap_or_else(|| renderer.default_size()),
|
self.text_size.unwrap_or_else(|| renderer.default_size()),
|
||||||
);
|
);
|
||||||
|
|
||||||
let adjusted = if self.anchored_children.is_empty() {
|
let adjusted = {
|
||||||
cursor
|
let metrics = state.line_metrics.borrow();
|
||||||
} else {
|
Point::new(cursor.x, buffer_y_to_widget_y(&metrics, cursor.y))
|
||||||
let line_h: f32 = line_height.into();
|
|
||||||
let line = (cursor.y / line_h).round() as usize;
|
|
||||||
let offset = items_height_before_line(&self.anchored_children, line);
|
|
||||||
Point::new(cursor.x, cursor.y + offset)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let position = adjusted + translation;
|
let position = adjusted + translation;
|
||||||
|
|
@ -630,6 +827,15 @@ pub struct State<Highlighter: text::Highlighter> {
|
||||||
/// Paragraphs built during draw() — kept alive so the renderer's Weak refs
|
/// Paragraphs built during draw() — kept alive so the renderer's Weak refs
|
||||||
/// survive until the prepare() phase processes them.
|
/// survive until the prepare() phase processes them.
|
||||||
retained_paragraphs: RefCell<Vec<iced_graphics::text::Paragraph>>,
|
retained_paragraphs: RefCell<Vec<iced_graphics::text::Paragraph>>,
|
||||||
|
/// Per-logical-line metrics published by `layout()`. Every consumer
|
||||||
|
/// (draw, cursor caret, click/drag hit-testing, IME) reads from this
|
||||||
|
/// same Vec — there is no parallel computation. Length = line_count
|
||||||
|
/// + 1, with the trailing sentinel marking widget/buffer y past the
|
||||||
|
/// last line.
|
||||||
|
line_metrics: RefCell<Vec<LineMetric>>,
|
||||||
|
/// Gutter strip width, also published by layout. Same single-source
|
||||||
|
/// rule: events translate click x by reading this, never recomputing.
|
||||||
|
gutter_width: std::cell::Cell<f32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
@ -707,6 +913,8 @@ where
|
||||||
highlighter_settings: self.highlighter_settings.clone(),
|
highlighter_settings: self.highlighter_settings.clone(),
|
||||||
highlighter_format_address: self.highlighter_format as usize,
|
highlighter_format_address: self.highlighter_format as usize,
|
||||||
retained_paragraphs: RefCell::new(Vec::new()),
|
retained_paragraphs: RefCell::new(Vec::new()),
|
||||||
|
line_metrics: RefCell::new(Vec::new()),
|
||||||
|
gutter_width: std::cell::Cell::new(0.0),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -756,8 +964,15 @@ where
|
||||||
.min_height(self.min_height)
|
.min_height(self.min_height)
|
||||||
.max_height(self.max_height);
|
.max_height(self.max_height);
|
||||||
|
|
||||||
|
let gw = self.gutter_width_for(internal.editor.line_count());
|
||||||
|
state.gutter_width.set(gw);
|
||||||
|
let effective_padding = Padding {
|
||||||
|
left: self.padding.left + gw,
|
||||||
|
..self.padding
|
||||||
|
};
|
||||||
|
|
||||||
internal.editor.update(
|
internal.editor.update(
|
||||||
limits.shrink(self.padding).max(),
|
limits.shrink(effective_padding).max(),
|
||||||
self.font.unwrap_or_else(|| renderer.default_font()),
|
self.font.unwrap_or_else(|| renderer.default_font()),
|
||||||
self.text_size.unwrap_or_else(|| renderer.default_size()),
|
self.text_size.unwrap_or_else(|| renderer.default_size()),
|
||||||
self.line_height,
|
self.line_height,
|
||||||
|
|
@ -768,19 +983,33 @@ where
|
||||||
let line_h: f32 = self.line_height.to_absolute(
|
let line_h: f32 = self.line_height.to_absolute(
|
||||||
self.text_size.unwrap_or_else(|| renderer.default_size()),
|
self.text_size.unwrap_or_else(|| renderer.default_size()),
|
||||||
).into();
|
).into();
|
||||||
let extra = total_items_height(&self.anchored_children);
|
|
||||||
|
|
||||||
// Compute child layouts at their stream positions
|
// Single source-of-truth: walk lines + anchored children once and
|
||||||
|
// build per-line metrics. Each LineMetric records the widget-y +
|
||||||
|
// buffer-y of that line's first visual row, plus the wrap count.
|
||||||
|
// Draw, cursor positioning, click/drag — every consumer reads from
|
||||||
|
// this same Vec so they cannot drift.
|
||||||
let mut child_nodes = Vec::with_capacity(self.anchored_children.len());
|
let mut child_nodes = Vec::with_capacity(self.anchored_children.len());
|
||||||
let child_limits = layout::Limits::new(
|
let child_limits = layout::Limits::new(
|
||||||
Size::ZERO,
|
Size::ZERO,
|
||||||
Size::new(limits.shrink(self.padding).max().width, f32::INFINITY),
|
Size::new(limits.shrink(effective_padding).max().width, f32::INFINITY),
|
||||||
);
|
);
|
||||||
let mut stream_y = 0.0f32;
|
let buffer = internal.editor.buffer();
|
||||||
|
let line_count = buffer.lines.len();
|
||||||
|
let mut metrics: Vec<LineMetric> = Vec::with_capacity(line_count + 1);
|
||||||
|
let mut widget_y = 0.0f32;
|
||||||
|
let mut buffer_y = 0.0f32;
|
||||||
let mut next_child = 0;
|
let mut next_child = 0;
|
||||||
let line_count = internal.editor.line_count();
|
|
||||||
for line in 0..line_count {
|
for line in 0..line_count {
|
||||||
stream_y += line_h;
|
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 });
|
||||||
|
let line_visual_h = visual_rows as f32 * line_h;
|
||||||
|
widget_y += line_visual_h;
|
||||||
|
buffer_y += line_visual_h;
|
||||||
while next_child < self.anchored_children.len()
|
while next_child < self.anchored_children.len()
|
||||||
&& self.anchored_children[next_child].after_line == line
|
&& self.anchored_children[next_child].after_line == line
|
||||||
{
|
{
|
||||||
|
|
@ -790,14 +1019,17 @@ where
|
||||||
renderer,
|
renderer,
|
||||||
&child_limits,
|
&child_limits,
|
||||||
);
|
);
|
||||||
node = node.move_to(Point::new(self.padding.left, self.padding.top + stream_y));
|
node = node.move_to(Point::new(
|
||||||
|
self.padding.left + gw,
|
||||||
|
self.padding.top + widget_y,
|
||||||
|
));
|
||||||
child.height = node.bounds().height;
|
child.height = node.bounds().height;
|
||||||
stream_y += child.height;
|
widget_y += child.height;
|
||||||
child_nodes.push(node);
|
child_nodes.push(node);
|
||||||
next_child += 1;
|
next_child += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Remaining children after last line
|
// Remaining children after last line — they sit below all text.
|
||||||
while next_child < self.anchored_children.len() {
|
while next_child < self.anchored_children.len() {
|
||||||
let child = &mut self.anchored_children[next_child];
|
let child = &mut self.anchored_children[next_child];
|
||||||
let mut node = child.element.as_widget_mut().layout(
|
let mut node = child.element.as_widget_mut().layout(
|
||||||
|
|
@ -805,18 +1037,27 @@ where
|
||||||
renderer,
|
renderer,
|
||||||
&child_limits,
|
&child_limits,
|
||||||
);
|
);
|
||||||
node = node.move_to(Point::new(self.padding.left, self.padding.top + stream_y));
|
node = node.move_to(Point::new(
|
||||||
|
self.padding.left + gw,
|
||||||
|
self.padding.top + widget_y,
|
||||||
|
));
|
||||||
child.height = node.bounds().height;
|
child.height = node.bounds().height;
|
||||||
stream_y += child.height;
|
widget_y += child.height;
|
||||||
child_nodes.push(node);
|
child_nodes.push(node);
|
||||||
next_child += 1;
|
next_child += 1;
|
||||||
}
|
}
|
||||||
|
// 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;
|
||||||
|
*state.line_metrics.borrow_mut() = metrics;
|
||||||
|
|
||||||
match self.height {
|
match self.height {
|
||||||
Length::Fill | Length::FillPortion(_) | Length::Fixed(_) => {
|
Length::Fill | Length::FillPortion(_) | Length::Fixed(_) => {
|
||||||
let mut size = limits.max();
|
// Fixed/Fill: caller specified the height. Honor it as-is —
|
||||||
size.height += extra;
|
// anchored items live within that height; trailing space
|
||||||
layout::Node::with_children(size, child_nodes)
|
// would otherwise create phantom gaps below the block.
|
||||||
|
layout::Node::with_children(limits.max(), child_nodes)
|
||||||
}
|
}
|
||||||
Length::Shrink => {
|
Length::Shrink => {
|
||||||
let min_bounds = internal.editor.min_bounds();
|
let min_bounds = internal.editor.min_bounds();
|
||||||
|
|
@ -923,13 +1164,13 @@ where
|
||||||
|
|
||||||
match update {
|
match update {
|
||||||
Update::Click(click) => {
|
Update::Click(click) => {
|
||||||
|
let gw = state.gutter_width.get();
|
||||||
let action = match click.kind() {
|
let action = match click.kind() {
|
||||||
mouse::click::Kind::Single => {
|
mouse::click::Kind::Single => {
|
||||||
let mut pos = click.position();
|
let mut pos = click.position();
|
||||||
if !self.anchored_children.is_empty() {
|
pos.x = (pos.x - gw).max(0.0);
|
||||||
let lc = self.content.0.borrow().editor.line_count();
|
let metrics = state.line_metrics.borrow();
|
||||||
pos.y = stream_y_to_text_y(pos.y, &self.anchored_children, line_h, lc);
|
pos.y = widget_y_to_buffer_y(&metrics, pos.y, line_h);
|
||||||
}
|
|
||||||
Action::Click(pos)
|
Action::Click(pos)
|
||||||
}
|
}
|
||||||
mouse::click::Kind::Double => Action::SelectWord,
|
mouse::click::Kind::Double => Action::SelectWord,
|
||||||
|
|
@ -944,11 +1185,11 @@ where
|
||||||
shell.capture_event();
|
shell.capture_event();
|
||||||
}
|
}
|
||||||
Update::Drag(position) => {
|
Update::Drag(position) => {
|
||||||
|
let gw = state.gutter_width.get();
|
||||||
let mut pos = position;
|
let mut pos = position;
|
||||||
if !self.anchored_children.is_empty() {
|
pos.x = (pos.x - gw).max(0.0);
|
||||||
let lc = self.content.0.borrow().editor.line_count();
|
let metrics = state.line_metrics.borrow();
|
||||||
pos.y = stream_y_to_text_y(pos.y, &self.anchored_children, line_h, lc);
|
pos.y = widget_y_to_buffer_y(&metrics, pos.y, line_h);
|
||||||
}
|
|
||||||
shell.publish(on_edit(Action::Drag(pos)));
|
shell.publish(on_edit(Action::Drag(pos)));
|
||||||
}
|
}
|
||||||
Update::Release => {
|
Update::Release => {
|
||||||
|
|
@ -1176,11 +1417,33 @@ where
|
||||||
style.background,
|
style.background,
|
||||||
);
|
);
|
||||||
|
|
||||||
let text_bounds = bounds.shrink(self.padding);
|
let gw = state.gutter_width.get();
|
||||||
|
let effective_padding = Padding {
|
||||||
|
left: self.padding.left + gw,
|
||||||
|
..self.padding
|
||||||
|
};
|
||||||
|
let text_bounds = bounds.shrink(effective_padding);
|
||||||
|
|
||||||
let text_size = self.text_size.unwrap_or_else(|| renderer.default_size());
|
let text_size = self.text_size.unwrap_or_else(|| renderer.default_size());
|
||||||
let line_h: f32 = self.line_height.to_absolute(text_size).into();
|
let line_h: f32 = self.line_height.to_absolute(text_size).into();
|
||||||
|
|
||||||
|
// Gutter background — only the strip below top_pad so the title-bar
|
||||||
|
// / traffic-light area doesn't get painted.
|
||||||
|
if self.show_gutter && self.padding.top < bounds.height {
|
||||||
|
let p = crate::palette::current();
|
||||||
|
renderer.fill_quad(
|
||||||
|
renderer::Quad {
|
||||||
|
bounds: Rectangle::new(
|
||||||
|
Point::new(bounds.x, bounds.y + self.padding.top),
|
||||||
|
Size::new(gw + self.padding.left, bounds.height - self.padding.top),
|
||||||
|
),
|
||||||
|
border: Border::default(),
|
||||||
|
..renderer::Quad::default()
|
||||||
|
},
|
||||||
|
Background::Color(p.crust),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if internal.editor.is_empty() {
|
if internal.editor.is_empty() {
|
||||||
if let Some(placeholder) = self.placeholder.clone() {
|
if let Some(placeholder) = self.placeholder.clone() {
|
||||||
renderer.fill_text(
|
renderer.fill_text(
|
||||||
|
|
@ -1200,19 +1463,13 @@ where
|
||||||
text_bounds,
|
text_bounds,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if self.anchored_children.is_empty() {
|
|
||||||
renderer.fill_editor(
|
|
||||||
&internal.editor,
|
|
||||||
text_bounds.position(),
|
|
||||||
style.value,
|
|
||||||
text_bounds,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
// Sequential stream: text lines (layer 0) interleaved with
|
// Sequential stream: text lines (layer 0) interleaved with
|
||||||
// anchored children (layer 1) in one continuous pass.
|
// anchored children (layer 1) in one continuous pass. Cursorline
|
||||||
|
// tint and gutter line numbers are drawn on the SAME y as the
|
||||||
|
// line's paragraph — single source of truth.
|
||||||
let buffer = internal.editor.buffer();
|
let buffer = internal.editor.buffer();
|
||||||
let line_count = buffer.lines.len();
|
let line_count = buffer.lines.len();
|
||||||
let mut stream_y = 0.0f32;
|
|
||||||
let mut child_idx = 0;
|
let mut child_idx = 0;
|
||||||
let children_layouts: Vec<_> = layout.children().collect();
|
let children_layouts: Vec<_> = layout.children().collect();
|
||||||
|
|
||||||
|
|
@ -1222,6 +1479,7 @@ where
|
||||||
{
|
{
|
||||||
let mut paras = state.retained_paragraphs.borrow_mut();
|
let mut paras = state.retained_paragraphs.borrow_mut();
|
||||||
paras.clear();
|
paras.clear();
|
||||||
|
let metrics = state.line_metrics.borrow();
|
||||||
for i in 0..line_count {
|
for i in 0..line_count {
|
||||||
let line_text = buffer.lines[i].text();
|
let line_text = buffer.lines[i].text();
|
||||||
let glyphs: Vec<cosmic_text::LayoutGlyph> =
|
let glyphs: Vec<cosmic_text::LayoutGlyph> =
|
||||||
|
|
@ -1229,9 +1487,10 @@ where
|
||||||
.map(|layouts| layouts.iter().flat_map(|l| l.glyphs.iter().cloned()).collect())
|
.map(|layouts| layouts.iter().flat_map(|l| l.glyphs.iter().cloned()).collect())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let spans = build_color_spans(line_text, &glyphs, f32::from(text_size));
|
let spans = build_color_spans(line_text, &glyphs, f32::from(text_size));
|
||||||
|
let visual_rows = metrics.get(i).map(|m| m.visual_rows).unwrap_or(1).max(1);
|
||||||
paras.push(iced_graphics::text::Paragraph::with_spans(Text {
|
paras.push(iced_graphics::text::Paragraph::with_spans(Text {
|
||||||
content: spans.as_slice(),
|
content: spans.as_slice(),
|
||||||
bounds: Size::new(text_bounds.width, line_h),
|
bounds: Size::new(text_bounds.width, visual_rows as f32 * line_h),
|
||||||
size: text_size,
|
size: text_size,
|
||||||
line_height: self.line_height,
|
line_height: self.line_height,
|
||||||
font,
|
font,
|
||||||
|
|
@ -1243,9 +1502,45 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let p = crate::palette::current();
|
||||||
|
let font_size_px: f32 = f32::from(text_size);
|
||||||
let paras = state.retained_paragraphs.borrow();
|
let paras = state.retained_paragraphs.borrow();
|
||||||
|
let metrics = state.line_metrics.borrow();
|
||||||
for line_i in 0..line_count {
|
for line_i in 0..line_count {
|
||||||
let y = text_bounds.y + line_i as f32 * line_h + stream_y;
|
// Pull line position from the Vec layout published.
|
||||||
|
let m = match metrics.get(line_i) {
|
||||||
|
Some(m) => m,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
let y = text_bounds.y + m.widget_y;
|
||||||
|
let row_h = m.visual_rows as f32 * line_h;
|
||||||
|
|
||||||
|
// Cursorline tint — full editor width (incl. gutter),
|
||||||
|
// covers all visual rows of the wrapped logical line.
|
||||||
|
if self.is_focused_block
|
||||||
|
&& self.cursor_line == Some(line_i)
|
||||||
|
&& self.line_indicator != crate::editor::LineIndicator::Off
|
||||||
|
{
|
||||||
|
let band = Color { a: 0.06, ..p.text };
|
||||||
|
renderer.fill_quad(
|
||||||
|
renderer::Quad {
|
||||||
|
bounds: Rectangle::new(
|
||||||
|
Point::new(bounds.x, y),
|
||||||
|
Size::new(bounds.width, row_h),
|
||||||
|
),
|
||||||
|
border: Border::default(),
|
||||||
|
..renderer::Quad::default()
|
||||||
|
},
|
||||||
|
Background::Color(band),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gutter — line decor stripe + line number, in the strip
|
||||||
|
// between bounds.x and text_bounds.x.
|
||||||
|
if self.show_gutter {
|
||||||
|
self.draw_gutter_line(renderer, line_i, bounds, y, line_h, gw, &p, font_size_px);
|
||||||
|
}
|
||||||
|
|
||||||
renderer.fill_paragraph(
|
renderer.fill_paragraph(
|
||||||
¶s[line_i],
|
¶s[line_i],
|
||||||
Point::new(text_bounds.x, y),
|
Point::new(text_bounds.x, y),
|
||||||
|
|
@ -1268,7 +1563,6 @@ where
|
||||||
_viewport,
|
_viewport,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
stream_y += self.anchored_children[child_idx].height;
|
|
||||||
child_idx += 1;
|
child_idx += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1293,14 +1587,9 @@ where
|
||||||
let translation = text_bounds.position() - Point::ORIGIN;
|
let translation = text_bounds.position() - Point::ORIGIN;
|
||||||
|
|
||||||
if let Some(focus) = state.focus.as_ref() {
|
if let Some(focus) = state.focus.as_ref() {
|
||||||
|
let metrics_for_cursor = state.line_metrics.borrow();
|
||||||
let adjust_y = |pos: Point| -> Point {
|
let adjust_y = |pos: Point| -> Point {
|
||||||
if self.anchored_children.is_empty() {
|
Point::new(pos.x, buffer_y_to_widget_y(&metrics_for_cursor, pos.y))
|
||||||
pos
|
|
||||||
} else {
|
|
||||||
let line = (pos.y / line_h).round() as usize;
|
|
||||||
let offset = items_height_before_line(&self.anchored_children, line);
|
|
||||||
Point::new(pos.x, pos.y + offset)
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
match internal.editor.selection() {
|
match internal.editor.selection() {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue