first pass of merging in warp (doesn't build)
This commit is contained in:
@@ -31,6 +31,7 @@ num-traits.workspace = true
|
||||
ordered-float.workspace = true
|
||||
html5ever.workspace = true
|
||||
markup5ever = "0.35.0"
|
||||
ipynb_parser.workspace = true
|
||||
markdown_parser.workspace = true
|
||||
mermaid_to_svg.workspace = true
|
||||
parking_lot.workspace = true
|
||||
@@ -45,8 +46,9 @@ string-offset.workspace = true
|
||||
sum_tree.workspace = true
|
||||
thiserror.workspace = true
|
||||
pathfinder_color = "0.5.0"
|
||||
unicode-width.workspace = true
|
||||
vec1.workspace = true
|
||||
galaxyui.workspace = true
|
||||
galaxyui_core.workspace = true
|
||||
galaxy_core.workspace = true
|
||||
galaxy_util.workspace = true
|
||||
rayon.workspace = true
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use std::fs;
|
||||
|
||||
use criterion::{Criterion, criterion_group, criterion_main};
|
||||
use galaxy_editor::content::{
|
||||
buffer::Buffer, selection_model::BufferSelectionModel, text::IndentBehavior,
|
||||
};
|
||||
use galaxyui::{App, ModelHandle};
|
||||
use rand::{SeedableRng, rngs::StdRng};
|
||||
use rand::SeedableRng;
|
||||
use rand::rngs::StdRng;
|
||||
use galaxy_editor::content::buffer::Buffer;
|
||||
use galaxy_editor::content::selection_model::BufferSelectionModel;
|
||||
use galaxy_editor::content::text::IndentBehavior;
|
||||
use galaxyui_core::{App, ModelHandle};
|
||||
|
||||
const EDIT_SAMPLE_SIZE: usize = 10;
|
||||
const MAX_EDIT_REPLACEMENT_LENGTH: usize = 20;
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Arc, Weak},
|
||||
};
|
||||
|
||||
use sum_tree::SumTree;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Weak};
|
||||
|
||||
use string_offset::CharOffset;
|
||||
use sum_tree::SumTree;
|
||||
|
||||
use super::text::BufferText;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "anchor_test.rs"]
|
||||
#[path = "anchor_tests.rs"]
|
||||
mod test;
|
||||
|
||||
/// Handle to a particular anchor. As long as there is an active handle, the
|
||||
|
||||
+7
-10
@@ -1,18 +1,15 @@
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use galaxyui::App;
|
||||
use string_offset::CharOffset;
|
||||
use sum_tree::SumTree;
|
||||
use galaxyui_core::App;
|
||||
|
||||
use super::{AnchorSide, Anchors};
|
||||
|
||||
use crate::content::{
|
||||
anchor::{Anchor, AnchorUpdate},
|
||||
buffer::Buffer,
|
||||
cursor::BufferSumTree,
|
||||
selection_model::BufferSelectionModel,
|
||||
text::IndentBehavior,
|
||||
};
|
||||
use string_offset::CharOffset;
|
||||
use crate::content::anchor::{Anchor, AnchorUpdate};
|
||||
use crate::content::buffer::Buffer;
|
||||
use crate::content::cursor::BufferSumTree;
|
||||
use crate::content::selection_model::BufferSelectionModel;
|
||||
use crate::content::text::IndentBehavior;
|
||||
|
||||
#[test]
|
||||
fn test_anchor_cleanup() {
|
||||
@@ -1,5 +1,10 @@
|
||||
use core::fmt;
|
||||
use galaxy_util::content_version::ContentVersion;
|
||||
use std::iter::{self, FusedIterator, once};
|
||||
use std::mem;
|
||||
use std::ops::Range;
|
||||
use std::sync::Arc;
|
||||
|
||||
use enum_iterator::all;
|
||||
use itertools::{Either, Itertools};
|
||||
use line_ending::LineEnding;
|
||||
use markdown_parser::{
|
||||
@@ -9,62 +14,55 @@ use markdown_parser::{
|
||||
};
|
||||
use num_traits::SaturatingSub;
|
||||
use pathfinder_color::ColorU;
|
||||
use rand::{Rng, distributions::Alphanumeric};
|
||||
use rand::Rng;
|
||||
use rand::distributions::Alphanumeric;
|
||||
use serde_yaml::Mapping;
|
||||
use std::{
|
||||
iter::{self, FusedIterator, once},
|
||||
mem,
|
||||
ops::Range,
|
||||
sync::Arc,
|
||||
};
|
||||
use vec1::{Vec1, vec1};
|
||||
|
||||
use super::{
|
||||
anchor::{Anchor, AnchorSide, Anchors},
|
||||
cursor::BufferCursor,
|
||||
edit::EditDelta,
|
||||
markdown::{BufferMarkdownParser, BufferToFormattedText, ExportedBufferBlocks, MarkdownStyle},
|
||||
selection::{Selection, TextStyleBias},
|
||||
text::{
|
||||
BlockCount, BlockLineBreakBehavior, BlockType, BufferBlockItem, BufferBlockStyle,
|
||||
BufferSummary, BufferText, BufferTextStyle, Bytes, CodeBlockType, IndentBehavior,
|
||||
LineCount, LinkCount, LinkMarker, MarkerDir, StyleSummary, SyntaxColorId, TextStyles,
|
||||
TextStylesWithMetadata, TextSummary, inline_to_text,
|
||||
},
|
||||
undo::{NonAtomicType, UndoActionType, UndoArg, UndoStack},
|
||||
validation::validate_content,
|
||||
};
|
||||
use galaxy_core::{platform::SessionPlatform, safe_error};
|
||||
|
||||
use crate::{
|
||||
content::{
|
||||
anchor::AnchorUpdate,
|
||||
core::{CoreEditorAction, CoreEditorActionType, RangeAnchors},
|
||||
cursor::BufferSumTree,
|
||||
edit::PreciseDelta,
|
||||
selection_model::{BufferSelectionModel, SelectionSnapshot},
|
||||
text::{ColorMarker, IndentUnit},
|
||||
undo::{ReversibleEditorActions, ReversibleSelectionState},
|
||||
version::BufferVersion,
|
||||
},
|
||||
multiline::{self, AnyMultilineString, LF, MultilineString},
|
||||
render::model::{EmbeddedItem, RenderedSelection, RenderedSelectionBias, RenderedSelectionSet},
|
||||
};
|
||||
use enum_iterator::all;
|
||||
use galaxyui::{AppContext, Entity, ModelContext};
|
||||
use galaxyui::{EntityId, ModelHandle, elements::ListIndentLevel};
|
||||
use galaxyui::{
|
||||
fonts::Weight,
|
||||
text::{TextBuffer, char_slice, point::Point},
|
||||
};
|
||||
use string_offset::{ByteOffset, CharOffset};
|
||||
use sum_tree::{SeekBias, SumTree};
|
||||
use vec1::{Vec1, vec1};
|
||||
use galaxy_core::platform::SessionPlatform;
|
||||
use galaxy_core::safe_error;
|
||||
use galaxy_util::content_version::ContentVersion;
|
||||
use galaxyui_core::elements::ListIndentLevel;
|
||||
use galaxyui_core::fonts::Weight;
|
||||
use galaxyui_core::text::point::Point;
|
||||
use galaxyui_core::text::{TextBuffer, char_slice};
|
||||
use galaxyui_core::{AppContext, Entity, EntityId, ModelContext, ModelHandle};
|
||||
|
||||
use super::anchor::{Anchor, AnchorSide, Anchors};
|
||||
use super::cursor::BufferCursor;
|
||||
use super::edit::EditDelta;
|
||||
use super::markdown::{
|
||||
BufferMarkdownParser, BufferToFormattedText, ExportedBufferBlocks, MarkdownStyle,
|
||||
};
|
||||
use super::selection::{Selection, TextStyleBias};
|
||||
use super::text::{
|
||||
BlockCount, BlockLineBreakBehavior, BlockType, BufferBlockItem, BufferBlockStyle,
|
||||
BufferSummary, BufferText, BufferTextStyle, Bytes, CodeBlockType, IndentBehavior, LineCount,
|
||||
LinkCount, LinkMarker, MarkerDir, StyleSummary, SyntaxColorId, TextStyles,
|
||||
TextStylesWithMetadata, TextSummary, inline_to_text,
|
||||
};
|
||||
use super::undo::{NonAtomicType, UndoActionType, UndoArg, UndoStack};
|
||||
use super::validation::validate_content;
|
||||
use crate::content::anchor::AnchorUpdate;
|
||||
use crate::content::core::{CoreEditorAction, CoreEditorActionType, RangeAnchors};
|
||||
use crate::content::cursor::BufferSumTree;
|
||||
use crate::content::edit::PreciseDelta;
|
||||
use crate::content::selection_model::{BufferSelectionModel, SelectionSnapshot};
|
||||
use crate::content::text::{ColorMarker, IndentUnit};
|
||||
use crate::content::undo::{ReversibleEditorActions, ReversibleSelectionState};
|
||||
use crate::content::version::BufferVersion;
|
||||
use crate::multiline::{self, AnyMultilineString, LF, MultilineString};
|
||||
use crate::render::model::{
|
||||
EmbeddedItem, RenderedSelection, RenderedSelectionBias, RenderedSelectionSet,
|
||||
};
|
||||
|
||||
/// Format of the passed in text.
|
||||
#[derive(Clone, Debug, Copy)]
|
||||
pub enum ContentFormat {
|
||||
Markdown,
|
||||
PlainText,
|
||||
Ipynb,
|
||||
}
|
||||
|
||||
/// Configuration struct that holds all the fields needed to reset the entire editor.
|
||||
@@ -95,6 +93,15 @@ impl<'a> InitialBufferState<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new InitialBufferState with Jupyter notebook (`.ipynb`) format
|
||||
pub fn ipynb(text: &'a str) -> Self {
|
||||
Self {
|
||||
text,
|
||||
format: ContentFormat::Ipynb,
|
||||
version: ContentVersion::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the content version
|
||||
pub fn with_version(mut self, version: ContentVersion) -> Self {
|
||||
self.version = version;
|
||||
@@ -538,6 +545,10 @@ impl BufferSnapshot {
|
||||
pub fn bytes(&self) -> Bytes<'_> {
|
||||
Bytes::from_sum_tree(&self.content, ByteOffset::from(0), self.byte_len)
|
||||
}
|
||||
|
||||
pub fn byte_len(&self) -> usize {
|
||||
self.byte_len.as_usize()
|
||||
}
|
||||
}
|
||||
|
||||
/// Model for storing the content of an editor.
|
||||
@@ -862,6 +873,31 @@ impl Buffer {
|
||||
)
|
||||
}
|
||||
|
||||
/// Construct a [`Buffer`] from the JSON contents of a `.ipynb` (Jupyter)
|
||||
/// notebook, converting it directly into formatted text.
|
||||
///
|
||||
/// Returns an [`ipynb_parser::IpynbError`] if the input is not a parseable
|
||||
/// nbformat v4 notebook, so callers can decide how to present invalid
|
||||
/// notebooks (e.g. routing to a raw text editor) rather than rendering a
|
||||
/// blank or misleading view.
|
||||
pub(crate) fn from_ipynb(
|
||||
ipynb: &str,
|
||||
embedded_item_conversion: Option<EmbeddedItemConversion>,
|
||||
tab_indentation: TabIndentation,
|
||||
selection_model: ModelHandle<BufferSelectionModel>,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) -> Result<Self, ipynb_parser::IpynbError> {
|
||||
let gfm_tables = galaxy_core::features::FeatureFlag::MarkdownTables.is_enabled();
|
||||
let formatted_text = ipynb_parser::ipynb_to_formatted_text(ipynb, gfm_tables)?;
|
||||
Ok(Self::from_formatted_text(
|
||||
formatted_text,
|
||||
embedded_item_conversion,
|
||||
tab_indentation,
|
||||
selection_model,
|
||||
ctx,
|
||||
))
|
||||
}
|
||||
|
||||
fn replace(
|
||||
&mut self,
|
||||
state: InitialBufferState,
|
||||
@@ -911,6 +947,28 @@ impl Buffer {
|
||||
selection_model.clone(),
|
||||
ctx,
|
||||
),
|
||||
ContentFormat::Ipynb => match Buffer::from_ipynb(
|
||||
state.text,
|
||||
callback,
|
||||
indentation,
|
||||
selection_model.clone(),
|
||||
ctx,
|
||||
) {
|
||||
Ok(buffer) => buffer,
|
||||
Err(e) => {
|
||||
safe_error! {
|
||||
safe: ("Failed to render Jupyter notebook; showing raw contents"),
|
||||
full: ("Failed to render Jupyter notebook: {e}")
|
||||
}
|
||||
Buffer::from_formatted_text(
|
||||
ipynb_parser::raw_fallback_formatted_text(state.text),
|
||||
callback,
|
||||
Box::new(|_, _| IndentBehavior::Ignore),
|
||||
selection_model.clone(),
|
||||
ctx,
|
||||
)
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Infer line ending from the new content and restore session_platform.
|
||||
@@ -4493,6 +4551,9 @@ impl Buffer {
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if edits.is_empty() {
|
||||
// TODO: This is temporary. We will add support to properly maintain the undo stack after incremental updates.
|
||||
self.reset_undo_stack();
|
||||
self.set_version(new_version);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -6118,5 +6179,5 @@ pub(super) enum BoundaryEdge {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "buffer_test.rs"]
|
||||
#[path = "buffer_tests.rs"]
|
||||
pub mod tests;
|
||||
|
||||
+123
-11
@@ -12,12 +12,19 @@ use pathfinder_color::ColorU;
|
||||
use rand::SeedableRng;
|
||||
use rand::rngs::StdRng;
|
||||
use serde_yaml::{Mapping, Value};
|
||||
use string_offset::{ByteOffset, CharOffset};
|
||||
use vec1::{Vec1, vec1};
|
||||
use galaxy_util::content_version::ContentVersion;
|
||||
use galaxyui_core::elements::ListIndentLevel;
|
||||
use galaxyui_core::text::point::Point;
|
||||
use galaxyui_core::{App, AppContext, ModelContext, ModelHandle, ReadModel};
|
||||
|
||||
use super::{BufferEvent, EditResult, ToBufferCharOffset};
|
||||
use crate::content::buffer::{
|
||||
AutoScrollBehavior, BufferEditAction, BufferSelectAction, EditOrigin, EmbeddedItemConversion,
|
||||
InitialBufferState, SelectionOffsets, StyledBlockBoundaryBehavior, StyledBufferBlock,
|
||||
StyledTextBlock, TabIndentation, ToBufferByteOffset, ToBufferPoint,
|
||||
AutoScrollBehavior, Buffer, BufferEditAction, BufferSelectAction, EditOrigin,
|
||||
EmbeddedItemConversion, InitialBufferState, SelectionOffsets, StyledBlockBoundaryBehavior,
|
||||
StyledBufferBlock, StyledBufferRun, StyledTextBlock, TabIndentation, ToBufferByteOffset,
|
||||
ToBufferPoint,
|
||||
};
|
||||
use crate::content::core::{CoreEditorAction, CoreEditorActionType};
|
||||
use crate::content::cursor::BufferSumTree;
|
||||
@@ -37,14 +44,6 @@ use crate::render::model::{
|
||||
EmbeddedItem, EmbeddedItemHTMLRepresentation, EmbeddedItemRichFormat, LaidOutEmbeddedItem,
|
||||
RenderedSelectionSet,
|
||||
};
|
||||
use galaxyui::elements::ListIndentLevel;
|
||||
use galaxyui::text::point::Point;
|
||||
use string_offset::ByteOffset;
|
||||
use string_offset::CharOffset;
|
||||
|
||||
use crate::content::buffer::{Buffer, StyledBufferRun};
|
||||
|
||||
use super::{BufferEvent, EditResult, ToBufferCharOffset};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TestEmbeddedItem {
|
||||
@@ -14166,6 +14165,65 @@ fn test_undo_redo_versions() {
|
||||
});
|
||||
}
|
||||
|
||||
/// Regression test: after a remote file save, the server's file-watcher sends a
|
||||
/// `BufferUpdatedPush` with the same content the client just saved.
|
||||
/// `insert_at_char_offset_ranges` is a no-op (content unchanged) and returns early
|
||||
/// without calling `set_version(new_version)`. But the caller updates
|
||||
/// `base_content_version` to `new_version` anyway, creating a mismatch that makes
|
||||
/// `has_unsaved_changes` return true — causing a spurious "Save changes?" dialog.
|
||||
#[test]
|
||||
fn test_insert_at_char_offset_ranges_noop_skips_set_version() {
|
||||
App::test((), |mut app| async move {
|
||||
let buffer = app.add_model(|_| Buffer::new(Box::new(|_, _| IndentBehavior::Ignore)));
|
||||
let selection = app.add_model(|_| BufferSelectionModel::new(buffer.clone()));
|
||||
|
||||
buffer.update(&mut app, |buffer, ctx| {
|
||||
// Step 1: Simulate initial file load — populate buffer and set version.
|
||||
buffer.replace_all("hello world", ctx);
|
||||
let load_version = ContentVersion::new();
|
||||
buffer.set_version(load_version);
|
||||
assert!(buffer.version_match(&load_version));
|
||||
|
||||
// Step 2: User edits the buffer.
|
||||
buffer.update_content(
|
||||
BufferEditAction::Insert {
|
||||
text: "!",
|
||||
style: TextStyles::default(),
|
||||
override_text_style: None,
|
||||
},
|
||||
EditOrigin::UserTyped,
|
||||
selection.clone(),
|
||||
ctx,
|
||||
);
|
||||
assert!(!buffer.version_match(&load_version));
|
||||
|
||||
// Step 3: Simulate save — capture the current buffer version as the base.
|
||||
let save_version = buffer.version();
|
||||
assert!(buffer.version_match(&save_version));
|
||||
|
||||
// Step 4: Simulate post-save server push with NO edits.
|
||||
// The server file-watcher detected the save, computed a diff against
|
||||
// the buffer content, and found zero changes — so the push carries
|
||||
// an empty edit list.
|
||||
let push_version = ContentVersion::new();
|
||||
buffer.insert_at_char_offset_ranges(vec![], push_version, ctx);
|
||||
|
||||
// Step 5: The caller (GlobalBufferModel) would set base_content_version = push_version.
|
||||
// version_match(push_version) should be true — the content hasn't changed.
|
||||
// BUG: insert_at_char_offset_ranges returned early without calling set_version,
|
||||
// so the buffer version is still save_version, not push_version.
|
||||
assert!(
|
||||
buffer.version_match(&push_version),
|
||||
"version_match should return true after a no-op insert_at_char_offset_ranges, \
|
||||
but the buffer version was not updated because the no-op early return \
|
||||
skipped set_version. Buffer version is {:?}, expected to match {:?}",
|
||||
buffer.version(),
|
||||
push_version,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_at_offsets() {
|
||||
App::test((), |mut app| async move {
|
||||
@@ -14217,6 +14275,60 @@ fn test_insert_at_offsets() {
|
||||
});
|
||||
}
|
||||
|
||||
/// Regression test for WARP-CLIENT-DEV-NYY: panic "Invalid edit range 4042..3982".
|
||||
///
|
||||
/// Root cause: `fuzzy_match_v4a_diffs` produces `DiffDelta`s with overlapping
|
||||
/// `replacement_line_range` values when multiple V4A hunks target the same
|
||||
/// region of a file (confirmed by the companion test
|
||||
/// `test_v4a_maa_crash_d71bf84b_no_overlapping_deltas` in the `ai` crate).
|
||||
///
|
||||
/// `CodeEditorModel::apply_diffs` converts those line ranges to char offsets
|
||||
/// and passes them to `insert_at_offsets`, which feeds them into
|
||||
/// `apply_core_edit_actions` without validating. The invalid range reaches
|
||||
/// `Buffer::edit`, which panics on the `debug_assert!`.
|
||||
///
|
||||
/// This test passes the exact Sentry crash values (`4042..3982`) to
|
||||
/// `insert_at_offsets` to confirm the editor does not defend against bad
|
||||
/// input from the diff layer.
|
||||
#[test]
|
||||
fn test_insert_at_offsets_overlapping_ranges_skipped() {
|
||||
App::test((), |mut app| async move {
|
||||
let buffer = app.add_model(|_| Buffer::new(Box::new(|_, _| IndentBehavior::Ignore)));
|
||||
let selection = app.add_model(|_| BufferSelectionModel::new(buffer.clone()));
|
||||
|
||||
buffer.update(&mut app, |buffer, ctx| {
|
||||
// Populate the buffer with enough content.
|
||||
let content = (0..200)
|
||||
.map(|i| format!("line_{:03}_content_padding", i))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
buffer.edit_internal_first_selection(
|
||||
CharOffset::from(1)..CharOffset::from(1),
|
||||
&content,
|
||||
Default::default(),
|
||||
selection.clone(),
|
||||
ctx,
|
||||
);
|
||||
|
||||
let original_text = buffer.text().into_string();
|
||||
|
||||
// Pass a range with start > end (the exact Sentry crash values).
|
||||
// After the fix in apply_core_edit_actions, the inverted range
|
||||
// should be skipped gracefully instead of panicking.
|
||||
let edits = Vec1::try_from_vec(vec![(
|
||||
"replacement\n".to_string(),
|
||||
CharOffset::from(4042)..CharOffset::from(3982),
|
||||
)])
|
||||
.unwrap();
|
||||
|
||||
buffer.insert_at_offsets(&edits, selection.clone(), ctx);
|
||||
|
||||
// Buffer should be unchanged — the invalid edit was skipped.
|
||||
assert_eq!(buffer.text().into_string(), original_text);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_plain_text() {
|
||||
App::test((), |mut app| async move {
|
||||
@@ -1,29 +1,45 @@
|
||||
use super::{
|
||||
buffer::{Buffer, EditOrigin, EditResult},
|
||||
cursor::BufferSumTree,
|
||||
edit::EditDelta,
|
||||
text::{
|
||||
BlockType, BufferTextStyle, ColorMarker, LinkCount, LinkMarker, MarkerDir, SyntaxColorId,
|
||||
TextStyles, TextStylesWithMetadata,
|
||||
},
|
||||
undo::{ReversibleEditorAction, UndoArg},
|
||||
};
|
||||
use crate::content::{
|
||||
anchor::{Anchor, AnchorSide, AnchorUpdate},
|
||||
buffer::{StyledBlockBoundaryBehavior, ToBufferByteOffset, ToBufferPoint},
|
||||
cursor::BufferCursor,
|
||||
edit::PreciseDelta,
|
||||
text::{
|
||||
BlockHeaderSize, BlockLineBreakBehavior, BufferBlockItem, BufferBlockStyle, BufferText,
|
||||
StyleSummary,
|
||||
},
|
||||
};
|
||||
use std::ops::Range;
|
||||
|
||||
use enum_iterator::all;
|
||||
use galaxyui::elements::ListIndentLevel;
|
||||
use markdown_parser::{FormattedText, FormattedTextFragment, FormattedTextLine};
|
||||
use std::ops::Range;
|
||||
use string_offset::CharOffset;
|
||||
use sum_tree::SumTree;
|
||||
use galaxyui_core::elements::ListIndentLevel;
|
||||
|
||||
use super::buffer::{Buffer, EditOrigin, EditResult};
|
||||
use super::cursor::BufferSumTree;
|
||||
use super::edit::EditDelta;
|
||||
use super::text::{
|
||||
BlockType, BufferTextStyle, ColorMarker, LinkCount, LinkMarker, MarkerDir, SyntaxColorId,
|
||||
TextStyles, TextStylesWithMetadata,
|
||||
};
|
||||
use super::undo::{ReversibleEditorAction, UndoArg};
|
||||
use crate::content::anchor::{Anchor, AnchorSide, AnchorUpdate};
|
||||
use crate::content::buffer::{StyledBlockBoundaryBehavior, ToBufferByteOffset, ToBufferPoint};
|
||||
use crate::content::cursor::BufferCursor;
|
||||
use crate::content::edit::PreciseDelta;
|
||||
use crate::content::text::{
|
||||
BlockHeaderSize, BlockLineBreakBehavior, BufferBlockItem, BufferBlockStyle, BufferText,
|
||||
StyleSummary,
|
||||
};
|
||||
|
||||
/// Placeholder shown in place of an embedded image whose `data:` payload
|
||||
/// exceeds the asset layer's render limit (see
|
||||
/// `asset_cache::data_uri_exceeds_limit`).
|
||||
const IMAGE_TOO_LARGE_PLACEHOLDER: &str = "Image too large to display";
|
||||
fn replace_oversized_data_uri_images(mut text: FormattedText) -> FormattedText {
|
||||
for line in text.lines.iter_mut() {
|
||||
if let FormattedTextLine::Image(image) = line
|
||||
&& asset_cache::data_uri_exceeds_limit(&image.source)
|
||||
{
|
||||
*line = FormattedTextLine::Line(vec![FormattedTextFragment::plain_text(
|
||||
IMAGE_TOO_LARGE_PLACEHOLDER,
|
||||
)]);
|
||||
}
|
||||
}
|
||||
text
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CoreEditorAction {
|
||||
@@ -215,6 +231,19 @@ impl Buffer {
|
||||
.resolve(&anchors.end)
|
||||
.expect("Anchor should exist");
|
||||
log::trace!("Start anchor => {edit_start}, end anchor => {edit_end}");
|
||||
|
||||
// Safety: if a previous edit in this batch caused the anchors for this
|
||||
// action to cross (start > end), skip the action rather than panicking.
|
||||
// This can happen when overlapping DiffDeltas slip through the diff
|
||||
// matching layer (see WARP-CLIENT-DEV-NYY).
|
||||
if edit_start > edit_end {
|
||||
log::warn!(
|
||||
"Skipping edit action with inverted range {edit_start}..{edit_end} \
|
||||
(anchors crossed after a prior edit in the same batch)"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
let edit_range = edit_start..edit_end;
|
||||
let replaced_points = self.offset_range_to_point_range(edit_range.clone());
|
||||
|
||||
@@ -293,6 +322,12 @@ impl Buffer {
|
||||
new_range_anchors.push(new_anchors);
|
||||
}
|
||||
|
||||
// If every action in the batch was skipped (e.g. all had inverted ranges),
|
||||
// there is nothing to commit — return early.
|
||||
if new_range_anchors.is_empty() {
|
||||
return EditResult::default();
|
||||
}
|
||||
|
||||
// Resolve each delta's new content range anchors against the final buffer state.
|
||||
// If a later action in the batch deletes the content an earlier action inserted,
|
||||
// the earlier action's anchors will have been invalidated — drop those deltas.
|
||||
@@ -525,6 +560,11 @@ impl Buffer {
|
||||
// as it is.
|
||||
let mut inherit_styling = source.from_user();
|
||||
|
||||
// Replace any embedded `data:` image whose payload exceeds the asset
|
||||
// layer's render limit with a visible placeholder before lowering lines
|
||||
// into the buffer, so an over-limit image surfaces a hint instead of
|
||||
// silently failing to load.
|
||||
let text = replace_oversized_data_uri_images(text);
|
||||
for line in text.lines {
|
||||
should_override_next_block_style = false;
|
||||
match line {
|
||||
@@ -1617,3 +1657,7 @@ fn maybe_push_new_block_marker(
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "core_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
use markdown_parser::{FormattedImage, FormattedText, FormattedTextLine};
|
||||
|
||||
use super::*;
|
||||
|
||||
/// A base64 `data:` image whose payload exceeds the asset layer's render limit.
|
||||
fn oversized_data_uri_image() -> FormattedImage {
|
||||
let payload = "A".repeat(asset_cache::MAX_DATA_URI_PAYLOAD_BYTES + 1);
|
||||
FormattedImage {
|
||||
alt_text: "output".to_string(),
|
||||
source: format!("data:image/png;base64,{payload}"),
|
||||
title: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replace_oversized_data_uri_images_swaps_in_placeholder() {
|
||||
// An in-limit image that must be left untouched.
|
||||
let small = FormattedImage {
|
||||
alt_text: "output".to_string(),
|
||||
source: "data:image/png;base64,iVBORw0KGgo=".to_string(),
|
||||
title: None,
|
||||
};
|
||||
|
||||
let text = FormattedText::new(vec![
|
||||
FormattedTextLine::Image(oversized_data_uri_image()),
|
||||
FormattedTextLine::Image(small.clone()),
|
||||
]);
|
||||
|
||||
let result = replace_oversized_data_uri_images(text);
|
||||
let lines: Vec<_> = result.lines.into_iter().collect();
|
||||
|
||||
// The oversized image becomes a visible placeholder text line ...
|
||||
assert!(matches!(&lines[0], FormattedTextLine::Line(_)));
|
||||
assert_eq!(
|
||||
lines[0].raw_text(),
|
||||
format!("{IMAGE_TOO_LARGE_PLACEHOLDER}\n")
|
||||
);
|
||||
|
||||
// ... while an in-limit image is left untouched.
|
||||
assert_eq!(lines[1], FormattedTextLine::Image(small));
|
||||
}
|
||||
@@ -455,5 +455,5 @@ where
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "cursor_test.rs"]
|
||||
#[path = "cursor_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
+2
-3
@@ -1,9 +1,8 @@
|
||||
use string_offset::CharOffset;
|
||||
use sum_tree::SumTree;
|
||||
|
||||
use crate::content::text::{BufferBlockStyle, BufferText, BufferTextStyle, MarkerDir};
|
||||
use string_offset::CharOffset;
|
||||
|
||||
use super::{BufferCursor, BufferSumTree};
|
||||
use crate::content::text::{BufferBlockStyle, BufferText, BufferTextStyle, MarkerDir};
|
||||
|
||||
/// Helper function to count the number of Text fragments in a SumTree
|
||||
fn count_text_fragments(tree: &SumTree<BufferText>) -> usize {
|
||||
@@ -3,8 +3,9 @@
|
||||
//! This module provides functionality for computing minimal diffs between two text strings,
|
||||
//! which is used for auto-reloading files without wiping undo history or disrupting anchors.
|
||||
|
||||
use imara_diff::{Algorithm, Diff, InternedInput, Token};
|
||||
use std::ops::Range;
|
||||
|
||||
use imara_diff::{Algorithm, Diff, InternedInput, Token};
|
||||
use string_offset::{ByteOffset, CharOffset};
|
||||
|
||||
use super::buffer::{Buffer, ToBufferCharOffset};
|
||||
|
||||
@@ -1,60 +1,72 @@
|
||||
use std::{
|
||||
cell::Cell,
|
||||
collections::HashMap,
|
||||
mem,
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use std::cell::Cell;
|
||||
use std::collections::HashMap;
|
||||
use std::mem;
|
||||
use std::ops::Range;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use galaxy_core::{features::FeatureFlag, ui::theme::Fill as ThemeFill};
|
||||
use galaxyui::{
|
||||
AppContext,
|
||||
assets::asset_cache::AssetSource,
|
||||
fonts::Weight,
|
||||
text::point::Point,
|
||||
text_layout::{StyleAndFont, TextAlignment},
|
||||
units::{IntoPixels, Pixels},
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use markdown_parser::{Hyperlink, TableAlignment};
|
||||
use num_traits::SaturatingSub;
|
||||
use rangemap::RangeSet;
|
||||
use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator};
|
||||
use string_offset::{ByteOffset, CharOffset};
|
||||
use urlocator::{UrlLocation, UrlLocator};
|
||||
use vec1::Vec1;
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use galaxy_core::ui::theme::Fill as ThemeFill;
|
||||
use galaxyui_core::assets::asset_cache::{AssetCache, AssetSource, AssetState};
|
||||
use galaxyui_core::fonts::Weight;
|
||||
use galaxyui_core::image_cache::ImageType;
|
||||
use galaxyui_core::text::char_slice;
|
||||
use galaxyui_core::text::point::Point;
|
||||
use galaxyui_core::text_layout::{StyleAndFont, TextAlignment};
|
||||
use galaxyui_core::units::{IntoPixels, Pixels};
|
||||
use galaxyui_core::{AppContext, SingletonEntity};
|
||||
|
||||
use crate::{
|
||||
parallel_util::Last,
|
||||
render::{
|
||||
TABLE_BASELINE_RATIO, TABLE_LINE_HEIGHT_RATIO,
|
||||
layout::{InlineTextLayoutInput, TextLayout, add_link_to_style_and_font},
|
||||
model::{
|
||||
BlockItem, BlockLocation, BlockSpacing, CellLayout, Cursor, Decoration, FrameOffset,
|
||||
HiddenBlockConfig, HorizontalRuleConfig, ImageBlockConfig, LaidOutEmbeddedItem,
|
||||
LaidOutTable, LineCount, OffsetMap, Paragraph, ParagraphBlock, ParagraphStyles,
|
||||
RenderLayoutOptions, SelectableTextRun, TableBlockConfig, TableStyle,
|
||||
gutter_expansion_button_types,
|
||||
},
|
||||
},
|
||||
use super::buffer::{StyledBufferBlock, StyledBufferRun, StyledTextBlock};
|
||||
use super::mermaid_diagram::{mermaid_asset_source, mermaid_diagram_layout};
|
||||
use super::text::{
|
||||
BufferBlockItem, BufferBlockStyle, CodeBlockType, FormattedTable, TableBlockCache,
|
||||
};
|
||||
use galaxyui::text::char_slice;
|
||||
use string_offset::{ByteOffset, CharOffset};
|
||||
|
||||
use super::{
|
||||
buffer::{StyledBufferBlock, StyledBufferRun, StyledTextBlock},
|
||||
mermaid_diagram::mermaid_diagram_layout,
|
||||
text::{BufferBlockItem, BufferBlockStyle, CodeBlockType, FormattedTable, TableBlockCache},
|
||||
use crate::parallel_util::Last;
|
||||
use crate::render::layout::{InlineTextLayoutInput, TextLayout, add_link_to_style_and_font};
|
||||
use crate::render::model::{
|
||||
BlockItem, BlockLocation, BlockSpacing, CellLayout, Cursor, Decoration, FrameOffset,
|
||||
HiddenBlockConfig, HorizontalRuleConfig, ImageBlockConfig, LaidOutEmbeddedItem, LaidOutTable,
|
||||
LineCount, OffsetMap, Paragraph, ParagraphBlock, ParagraphStyles, RenderLayoutOptions,
|
||||
SelectableTextRun, TableBlockConfig, TableStyle, gutter_expansion_button_types,
|
||||
};
|
||||
use crate::render::{TABLE_BASELINE_RATIO, TABLE_LINE_HEIGHT_RATIO};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "edit_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
#[cfg(any(test, feature = "test-util"))]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn layout_mermaid_block_for_test(
|
||||
text_block: StyledTextBlock,
|
||||
layout: &TextLayout,
|
||||
layout_options: RenderLayoutOptions,
|
||||
app: &AppContext,
|
||||
) -> Result<(BlockItem, bool)> {
|
||||
let task = LayoutTask::from_styled_block(
|
||||
StyledBufferBlock::Text(text_block),
|
||||
layout,
|
||||
&layout_options,
|
||||
CharOffset::from(1),
|
||||
app,
|
||||
None,
|
||||
);
|
||||
task.run(layout, BlockLocation::Middle, false)
|
||||
}
|
||||
|
||||
/// Resolve an image source path to an AssetSource.
|
||||
///
|
||||
/// Supports the following markdown image formats per the CommonMark spec:
|
||||
/// https://spec.commonmark.org/0.31.2/#images
|
||||
/// - Inline data: base64 `data:` URIs (e.g. notebook image outputs)
|
||||
/// - URLs: `http://` or `https://` prefixed paths
|
||||
/// - Absolute paths: paths starting with `/`
|
||||
/// - Relative paths: all other paths, resolved relative to the document location
|
||||
@@ -65,11 +77,14 @@ pub fn resolve_asset_source_relative_to_directory(
|
||||
source: &str,
|
||||
base_directory: Option<&Path>,
|
||||
) -> AssetSource {
|
||||
if source.starts_with("http://") || source.starts_with("https://") {
|
||||
if let Some(data_uri_source) = asset_cache::data_uri_source(source) {
|
||||
data_uri_source
|
||||
} else if source.starts_with("http://") || source.starts_with("https://") {
|
||||
asset_cache::url_source(source)
|
||||
} else if source.starts_with("/") {
|
||||
AssetSource::LocalFile {
|
||||
path: source.to_string(),
|
||||
content_version: None,
|
||||
}
|
||||
} else {
|
||||
let resolved_path = if let Some(base_directory) = base_directory {
|
||||
@@ -83,6 +98,7 @@ pub fn resolve_asset_source_relative_to_directory(
|
||||
Ok(canon) => canon.to_string_lossy().to_string(),
|
||||
Err(_) => resolved_path.to_string_lossy().to_string(),
|
||||
},
|
||||
content_version: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,11 +113,14 @@ pub fn resolve_asset_source_relative_to_directory(
|
||||
source: &str,
|
||||
_base_directory: Option<&Path>,
|
||||
) -> AssetSource {
|
||||
if source.starts_with("http://") || source.starts_with("https://") {
|
||||
if let Some(data_uri_source) = asset_cache::data_uri_source(source) {
|
||||
data_uri_source
|
||||
} else if source.starts_with("http://") || source.starts_with("https://") {
|
||||
asset_cache::url_source(source)
|
||||
} else {
|
||||
AssetSource::LocalFile {
|
||||
path: source.to_string(),
|
||||
content_version: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -487,7 +506,7 @@ impl EditDelta {
|
||||
self,
|
||||
layout: &TextLayout,
|
||||
document_path: Option<&Path>,
|
||||
layout_options: RenderLayoutOptions,
|
||||
layout_options: &RenderLayoutOptions,
|
||||
hidden_ranges: Option<RangeSet<CharOffset>>,
|
||||
app: &AppContext,
|
||||
) -> LaidOutRenderDelta {
|
||||
@@ -505,10 +524,12 @@ impl EditDelta {
|
||||
if content_length == CharOffset::zero() {
|
||||
None
|
||||
} else {
|
||||
let block_start = current_offset;
|
||||
let task = LayoutTask::from_styled_block(
|
||||
block,
|
||||
layout,
|
||||
layout_options,
|
||||
block_start,
|
||||
app,
|
||||
document_path,
|
||||
);
|
||||
@@ -646,6 +667,14 @@ enum LayoutTask {
|
||||
asset_source: AssetSource,
|
||||
config: ImageBlockConfig,
|
||||
},
|
||||
/// A Mermaid-labeled code block whose contents are not currently renderable as a diagram
|
||||
/// (either the render hasn't completed yet or it failed to parse). This lays out identically
|
||||
/// to [`Self::Text`] but carries the Mermaid asset source so that the view layer can watch
|
||||
/// for the asset to finish loading and re-run layout if it becomes parseable.
|
||||
MermaidCodeFallback {
|
||||
text_block: StyledTextBlock,
|
||||
pending_mermaid_asset: Option<AssetSource>,
|
||||
},
|
||||
/// A horizontal rule, which requires no layout.
|
||||
HorizontalRule(HorizontalRuleConfig),
|
||||
/// An image, which requires no layout.
|
||||
@@ -668,7 +697,8 @@ impl LayoutTask {
|
||||
fn from_styled_block(
|
||||
content: StyledBufferBlock,
|
||||
layout: &TextLayout,
|
||||
layout_options: RenderLayoutOptions,
|
||||
layout_options: &RenderLayoutOptions,
|
||||
block_start: CharOffset,
|
||||
app: &AppContext,
|
||||
document_path: Option<&Path>,
|
||||
) -> Self {
|
||||
@@ -720,29 +750,95 @@ impl LayoutTask {
|
||||
}
|
||||
},
|
||||
StyledBufferBlock::Text(text_block) => {
|
||||
if layout_options.render_mermaid_diagrams
|
||||
&& matches!(
|
||||
text_block.style,
|
||||
BufferBlockStyle::CodeBlock {
|
||||
code_block_type: CodeBlockType::Mermaid,
|
||||
}
|
||||
)
|
||||
{
|
||||
let is_mermaid = matches!(
|
||||
text_block.style,
|
||||
BufferBlockStyle::CodeBlock {
|
||||
code_block_type: CodeBlockType::Mermaid,
|
||||
}
|
||||
);
|
||||
let is_user_rendered = layout_options.mermaid_render_offsets.contains(&block_start);
|
||||
let should_render =
|
||||
is_mermaid && (layout_options.render_mermaid_diagrams || is_user_rendered);
|
||||
|
||||
if should_render {
|
||||
let source = text_block
|
||||
.block
|
||||
.iter()
|
||||
.map(|run| run.run.as_str())
|
||||
.collect::<String>();
|
||||
let spacing = layout
|
||||
.rich_text_styles()
|
||||
.block_spacings
|
||||
.from_block_style(&text_block.style);
|
||||
let (asset_source, config) =
|
||||
mermaid_diagram_layout(&source, layout, spacing, app);
|
||||
Self::MermaidDiagram {
|
||||
text_block,
|
||||
asset_source,
|
||||
config,
|
||||
// Empty Mermaid blocks can never be rendered as a diagram, so avoid
|
||||
// kicking off any render work and fall through to a plain code block.
|
||||
if source.trim().is_empty() {
|
||||
return Self::MermaidCodeFallback {
|
||||
text_block,
|
||||
pending_mermaid_asset: None,
|
||||
};
|
||||
}
|
||||
|
||||
let asset_source = mermaid_asset_source(&source);
|
||||
let asset_cache = AssetCache::as_ref(app);
|
||||
match asset_cache.load_asset::<ImageType>(asset_source.clone()) {
|
||||
AssetState::Loaded { .. } => {
|
||||
let spacing = layout
|
||||
.rich_text_styles()
|
||||
.block_spacings
|
||||
.from_block_style(&text_block.style);
|
||||
let (asset_source, config) =
|
||||
mermaid_diagram_layout(&source, layout, spacing, app);
|
||||
Self::MermaidDiagram {
|
||||
text_block,
|
||||
asset_source,
|
||||
config,
|
||||
}
|
||||
}
|
||||
AssetState::Loading { .. } => {
|
||||
if is_user_rendered {
|
||||
// User explicitly chose Rendered — show diagram frame immediately
|
||||
// (render element will display loading placeholder).
|
||||
let spacing = layout
|
||||
.rich_text_styles()
|
||||
.block_spacings
|
||||
.from_block_style(&text_block.style);
|
||||
let (asset_source, config) =
|
||||
mermaid_diagram_layout(&source, layout, spacing, app);
|
||||
Self::MermaidDiagram {
|
||||
text_block,
|
||||
asset_source,
|
||||
config,
|
||||
}
|
||||
} else {
|
||||
// Auto-render mode: stay in code-block view while loading.
|
||||
Self::MermaidCodeFallback {
|
||||
text_block,
|
||||
pending_mermaid_asset: Some(asset_source),
|
||||
}
|
||||
}
|
||||
}
|
||||
AssetState::FailedToLoad(_) | AssetState::Evicted => {
|
||||
if is_user_rendered {
|
||||
// User explicitly chose Rendered — show diagram frame with error.
|
||||
let spacing = layout
|
||||
.rich_text_styles()
|
||||
.block_spacings
|
||||
.from_block_style(&text_block.style);
|
||||
let (asset_source, config) =
|
||||
mermaid_diagram_layout(&source, layout, spacing, app);
|
||||
Self::MermaidDiagram {
|
||||
text_block,
|
||||
asset_source,
|
||||
config,
|
||||
}
|
||||
} else {
|
||||
// Rendering failed. Keep in code-block view so the user can
|
||||
// see and edit the raw text. No need to keep watching: the
|
||||
// asset state will not change until the source (and thus the
|
||||
// asset key) does.
|
||||
Self::MermaidCodeFallback {
|
||||
text_block,
|
||||
pending_mermaid_asset: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Self::Text(text_block)
|
||||
@@ -792,6 +888,26 @@ impl LayoutTask {
|
||||
))
|
||||
}
|
||||
Self::Text(text_block) => layout_text_block(text_block, layout, location, is_hidden),
|
||||
Self::MermaidCodeFallback {
|
||||
text_block,
|
||||
pending_mermaid_asset,
|
||||
} => {
|
||||
let (block_item, has_trailing_newline) =
|
||||
layout_text_block(text_block, layout, location, is_hidden)?;
|
||||
let block_item = match block_item {
|
||||
BlockItem::RunnableCodeBlock {
|
||||
paragraph_block,
|
||||
code_block_type,
|
||||
..
|
||||
} => BlockItem::RunnableCodeBlock {
|
||||
paragraph_block,
|
||||
code_block_type,
|
||||
pending_mermaid_asset,
|
||||
},
|
||||
other => other,
|
||||
};
|
||||
Ok((block_item, has_trailing_newline))
|
||||
}
|
||||
Self::MermaidDiagram {
|
||||
text_block,
|
||||
asset_source,
|
||||
@@ -952,6 +1068,7 @@ fn layout_text_block(
|
||||
BlockItem::RunnableCodeBlock {
|
||||
paragraph_block,
|
||||
code_block_type,
|
||||
pending_mermaid_asset: None,
|
||||
}
|
||||
})
|
||||
.ok_or_else(|| anyhow!("Code block should have at least one paragraph")),
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
use std::path::Path;
|
||||
|
||||
use string_offset::CharOffset;
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use galaxyui_core::assets::asset_cache::{AssetCache, AssetSource, AssetState};
|
||||
use galaxyui_core::fonts::{Properties, Style, Weight};
|
||||
use galaxyui_core::image_cache::ImageType;
|
||||
use galaxyui_core::text_layout::{LayoutCache, StyleAndFont, TextStyle};
|
||||
use galaxyui_core::{App, SingletonEntity};
|
||||
|
||||
use super::{
|
||||
BlockLocation, LayOutArgs, layout_mermaid_diagram_block, layout_table_block, layout_text_block,
|
||||
};
|
||||
use crate::{
|
||||
content::{
|
||||
buffer::{StyledBufferRun, StyledTextBlock},
|
||||
edit::{ParsedUrl, highlight_urls, resolve_asset_source_relative_to_directory},
|
||||
mermaid_diagram::{mermaid_asset_source, mermaid_diagram_layout},
|
||||
text::{BufferBlockStyle, CodeBlockType, TextStylesWithMetadata},
|
||||
},
|
||||
render::{
|
||||
layout::{TextLayout, add_link_to_style_and_font, markdown_inline_to_text_and_style_runs},
|
||||
model::{BlockItem, test_utils::TEST_STYLES},
|
||||
},
|
||||
use crate::content::buffer::{StyledBufferRun, StyledTextBlock};
|
||||
use crate::content::edit::{
|
||||
ParsedUrl, highlight_urls, layout_mermaid_block_for_test,
|
||||
resolve_asset_source_relative_to_directory,
|
||||
};
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use galaxyui::{
|
||||
App, SingletonEntity,
|
||||
assets::asset_cache::{AssetCache, AssetSource, AssetState},
|
||||
fonts::{Properties, Style, Weight},
|
||||
image_cache::ImageType,
|
||||
text_layout::{LayoutCache, StyleAndFont, TextStyle},
|
||||
use crate::content::mermaid_diagram::{mermaid_asset_source, mermaid_diagram_layout};
|
||||
use crate::content::text::{BufferBlockStyle, CodeBlockType, TextStylesWithMetadata};
|
||||
use crate::render::layout::{
|
||||
TextLayout, add_link_to_style_and_font, markdown_inline_to_text_and_style_runs,
|
||||
};
|
||||
use std::path::Path;
|
||||
use string_offset::CharOffset;
|
||||
use crate::render::model::test_utils::TEST_STYLES;
|
||||
use crate::render::model::{BlockItem, RenderLayoutOptions};
|
||||
|
||||
#[test]
|
||||
fn test_highlight_urls() {
|
||||
@@ -362,13 +362,12 @@ fn test_layout_mermaid_block_uses_loaded_svg_aspect_ratio() {
|
||||
..
|
||||
} => {
|
||||
let intrinsic_size = svg.size();
|
||||
let expected_width = (800.
|
||||
let expected_width = 800.
|
||||
- TEST_STYLES
|
||||
.block_spacings
|
||||
.from_block_style(&block_style)
|
||||
.x_axis_offset()
|
||||
.as_f32())
|
||||
.min(intrinsic_size.width());
|
||||
.as_f32();
|
||||
let expected_height =
|
||||
expected_width * intrinsic_size.height() / intrinsic_size.width();
|
||||
assert_eq!(*content_length, CharOffset::from(content.chars().count()));
|
||||
@@ -384,13 +383,283 @@ fn test_layout_mermaid_block_uses_loaded_svg_aspect_ratio() {
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unloaded_mermaid_diagram_uses_stable_full_width_placeholder_height() {
|
||||
App::test((), |app| async move {
|
||||
let _flag = FeatureFlag::MarkdownMermaid.override_enabled(true);
|
||||
app.read(|ctx| {
|
||||
let layout_cache = LayoutCache::new();
|
||||
let text_layout = TextLayout::new(
|
||||
&layout_cache,
|
||||
ctx.font_cache().text_layout_system(),
|
||||
&TEST_STYLES,
|
||||
800.,
|
||||
);
|
||||
let contents = "graph TD\nA[Unloaded] --> B[Placeholder]\n";
|
||||
let block_style = BufferBlockStyle::CodeBlock {
|
||||
code_block_type: CodeBlockType::Mermaid,
|
||||
};
|
||||
let spacing = TEST_STYLES.block_spacings.from_block_style(&block_style);
|
||||
let (_asset_source, config) =
|
||||
mermaid_diagram_layout(contents, &text_layout, spacing, ctx);
|
||||
let expected_width = 800. - spacing.x_axis_offset().as_f32();
|
||||
let expected_height = TEST_STYLES.base_line_height().as_f32() * 10.;
|
||||
|
||||
assert!(
|
||||
(config.width.as_f32() - expected_width).abs() < 0.5,
|
||||
"expected unloaded Mermaid diagram width {} to use full available width {}",
|
||||
config.width.as_f32(),
|
||||
expected_width,
|
||||
);
|
||||
assert!(
|
||||
(config.height.as_f32() - expected_height).abs() < 0.5,
|
||||
"expected unloaded Mermaid diagram height {} to use stable placeholder height {}",
|
||||
config.height.as_f32(),
|
||||
expected_height,
|
||||
);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
fn mermaid_code_block(contents: &str) -> StyledTextBlock {
|
||||
let block_style = BufferBlockStyle::CodeBlock {
|
||||
code_block_type: CodeBlockType::Mermaid,
|
||||
};
|
||||
StyledTextBlock {
|
||||
block: vec![StyledBufferRun {
|
||||
run: contents.to_string(),
|
||||
text_styles: TextStylesWithMetadata::default(),
|
||||
block_style: block_style.clone(),
|
||||
}],
|
||||
style: block_style,
|
||||
content_length: CharOffset::from(contents.chars().count()),
|
||||
}
|
||||
}
|
||||
|
||||
fn mermaid_layout_options() -> RenderLayoutOptions {
|
||||
RenderLayoutOptions {
|
||||
render_mermaid_diagrams: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_mermaid_block_lays_out_as_code_block() {
|
||||
App::test((), |app| async move {
|
||||
let _flag = FeatureFlag::MarkdownMermaid.override_enabled(true);
|
||||
app.read(|ctx| {
|
||||
let layout_cache = LayoutCache::new();
|
||||
let text_layout = TextLayout::new(
|
||||
&layout_cache,
|
||||
ctx.font_cache().text_layout_system(),
|
||||
&TEST_STYLES,
|
||||
800.,
|
||||
);
|
||||
let block = mermaid_code_block("\n");
|
||||
let (item, _has_trailing_newline) =
|
||||
layout_mermaid_block_for_test(block, &text_layout, mermaid_layout_options(), ctx)
|
||||
.expect("layout should succeed");
|
||||
|
||||
match item {
|
||||
BlockItem::RunnableCodeBlock {
|
||||
code_block_type,
|
||||
pending_mermaid_asset,
|
||||
..
|
||||
} => {
|
||||
assert_eq!(code_block_type, CodeBlockType::Mermaid);
|
||||
assert!(
|
||||
pending_mermaid_asset.is_none(),
|
||||
"empty Mermaid blocks should not trigger a render"
|
||||
);
|
||||
}
|
||||
other => panic!("expected code block, got {other:?}"),
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_non_parseable_mermaid_block_lays_out_as_code_block() {
|
||||
App::test((), |app| async move {
|
||||
let _flag = FeatureFlag::MarkdownMermaid.override_enabled(true);
|
||||
app.read(|ctx| {
|
||||
let layout_cache = LayoutCache::new();
|
||||
let text_layout = TextLayout::new(
|
||||
&layout_cache,
|
||||
ctx.font_cache().text_layout_system(),
|
||||
&TEST_STYLES,
|
||||
800.,
|
||||
);
|
||||
let block = mermaid_code_block("echo hi\n");
|
||||
let (item, _) =
|
||||
layout_mermaid_block_for_test(block, &text_layout, mermaid_layout_options(), ctx)
|
||||
.expect("layout should succeed");
|
||||
|
||||
// On first sight, the asset is still loading. We defer the Mermaid-diagram
|
||||
// path until the render either succeeds or fails, so the block should lay out
|
||||
// as a code block with the asset source attached so the view layer can watch it.
|
||||
match item {
|
||||
BlockItem::RunnableCodeBlock {
|
||||
code_block_type,
|
||||
pending_mermaid_asset,
|
||||
..
|
||||
} => {
|
||||
assert_eq!(code_block_type, CodeBlockType::Mermaid);
|
||||
assert!(
|
||||
pending_mermaid_asset.is_some(),
|
||||
"a freshly-seen Mermaid source should schedule an asset load"
|
||||
);
|
||||
}
|
||||
other => panic!("expected code block, got {other:?}"),
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_mermaid_block_stays_as_code_block_after_load_fails() {
|
||||
App::test((), |app| async move {
|
||||
let _flag = FeatureFlag::MarkdownMermaid.override_enabled(true);
|
||||
let contents = "echo hi\n";
|
||||
let asset_source = mermaid_asset_source(contents);
|
||||
|
||||
// Drive the asset load to completion (it should fail, since `echo hi` isn't
|
||||
// valid Mermaid).
|
||||
let pending = app.read(|ctx| {
|
||||
let asset_cache = AssetCache::as_ref(ctx);
|
||||
match asset_cache.load_asset::<ImageType>(asset_source.clone()) {
|
||||
AssetState::Loading { handle } => handle.when_loaded(asset_cache),
|
||||
_ => None,
|
||||
}
|
||||
});
|
||||
if let Some(future) = pending {
|
||||
future.await;
|
||||
}
|
||||
|
||||
app.read(|ctx| {
|
||||
let asset_cache = AssetCache::as_ref(ctx);
|
||||
assert!(
|
||||
matches!(
|
||||
asset_cache.load_asset::<ImageType>(asset_source.clone()),
|
||||
AssetState::FailedToLoad(_)
|
||||
),
|
||||
"Mermaid render for invalid source should have failed"
|
||||
);
|
||||
|
||||
let layout_cache = LayoutCache::new();
|
||||
let text_layout = TextLayout::new(
|
||||
&layout_cache,
|
||||
ctx.font_cache().text_layout_system(),
|
||||
&TEST_STYLES,
|
||||
800.,
|
||||
);
|
||||
let block = mermaid_code_block(contents);
|
||||
let (item, _) =
|
||||
layout_mermaid_block_for_test(block, &text_layout, mermaid_layout_options(), ctx)
|
||||
.expect("layout should succeed");
|
||||
|
||||
match item {
|
||||
BlockItem::RunnableCodeBlock {
|
||||
code_block_type,
|
||||
pending_mermaid_asset,
|
||||
..
|
||||
} => {
|
||||
assert_eq!(code_block_type, CodeBlockType::Mermaid);
|
||||
// Once the asset load has failed we don't need to keep watching the
|
||||
// asset handle; the state won't flip again until the source changes.
|
||||
assert!(
|
||||
pending_mermaid_asset.is_none(),
|
||||
"no watcher is needed after a Mermaid render failure"
|
||||
);
|
||||
}
|
||||
other => panic!("expected code block, got {other:?}"),
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_valid_mermaid_block_lays_out_as_diagram_after_load() {
|
||||
App::test((), |app| async move {
|
||||
let _flag = FeatureFlag::MarkdownMermaid.override_enabled(true);
|
||||
let contents = "graph TD\nA[Start] --> B[Finish]\n";
|
||||
let asset_source = mermaid_asset_source(contents);
|
||||
|
||||
// Drive the async Mermaid render to completion.
|
||||
let pending = app.read(|ctx| {
|
||||
let asset_cache = AssetCache::as_ref(ctx);
|
||||
match asset_cache.load_asset::<ImageType>(asset_source.clone()) {
|
||||
AssetState::Loading { handle } => handle.when_loaded(asset_cache),
|
||||
_ => None,
|
||||
}
|
||||
});
|
||||
if let Some(future) = pending {
|
||||
future.await;
|
||||
}
|
||||
|
||||
app.read(|ctx| {
|
||||
let layout_cache = LayoutCache::new();
|
||||
let text_layout = TextLayout::new(
|
||||
&layout_cache,
|
||||
ctx.font_cache().text_layout_system(),
|
||||
&TEST_STYLES,
|
||||
800.,
|
||||
);
|
||||
let block = mermaid_code_block(contents);
|
||||
let (item, _) =
|
||||
layout_mermaid_block_for_test(block, &text_layout, mermaid_layout_options(), ctx)
|
||||
.expect("layout should succeed");
|
||||
|
||||
assert!(
|
||||
matches!(item, BlockItem::MermaidDiagram { .. }),
|
||||
"expected MermaidDiagram block once the asset is loaded, got {item:?}"
|
||||
);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mermaid_block_skipped_when_render_disabled() {
|
||||
App::test((), |app| async move {
|
||||
let _flag = FeatureFlag::MarkdownMermaid.override_enabled(true);
|
||||
app.read(|ctx| {
|
||||
let layout_cache = LayoutCache::new();
|
||||
let text_layout = TextLayout::new(
|
||||
&layout_cache,
|
||||
ctx.font_cache().text_layout_system(),
|
||||
&TEST_STYLES,
|
||||
800.,
|
||||
);
|
||||
let block = mermaid_code_block("graph TD\nA --> B\n");
|
||||
let options = RenderLayoutOptions {
|
||||
render_mermaid_diagrams: false,
|
||||
..Default::default()
|
||||
};
|
||||
let (item, _) = layout_mermaid_block_for_test(block, &text_layout, options, ctx)
|
||||
.expect("layout should succeed");
|
||||
|
||||
// When Mermaid rendering is disabled globally, the block should lay out as a
|
||||
// plain code block with no asset source attached.
|
||||
match item {
|
||||
BlockItem::RunnableCodeBlock {
|
||||
pending_mermaid_asset,
|
||||
..
|
||||
} => {
|
||||
assert!(pending_mermaid_asset.is_none());
|
||||
}
|
||||
other => panic!("expected code block, got {other:?}"),
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_asset_source_relative_to_directory_uses_base_directory() {
|
||||
let asset_source =
|
||||
resolve_asset_source_relative_to_directory("diagram.png", Some(Path::new("/tmp/session")));
|
||||
|
||||
match asset_source {
|
||||
AssetSource::LocalFile { path } => {
|
||||
AssetSource::LocalFile { path, .. } => {
|
||||
assert_eq!(Path::new(&path), Path::new("/tmp/session/diagram.png"));
|
||||
}
|
||||
source => panic!("expected local file asset source, got {source:?}"),
|
||||
|
||||
@@ -11,31 +11,24 @@
|
||||
//! optimizations rely on fast substring searches (like highly-optimized platform-specific
|
||||
//! `memchr` implementations) that we can't use with a non-contiguous buffer.
|
||||
|
||||
use std::{borrow::Cow, future::Future};
|
||||
use std::borrow::Cow;
|
||||
use std::future::Future;
|
||||
|
||||
use anyhow::{Context, bail};
|
||||
use rangemap::RangeSet;
|
||||
use regex_automata::{
|
||||
Anchored, Input, MatchError, MatchKind,
|
||||
hybrid::{
|
||||
BuildError, LazyStateID,
|
||||
dfa::{Cache, DFA},
|
||||
},
|
||||
nfa::thompson,
|
||||
util::syntax::Config,
|
||||
};
|
||||
use regex_automata::hybrid::dfa::{Cache, DFA};
|
||||
use regex_automata::hybrid::{BuildError, LazyStateID};
|
||||
use regex_automata::nfa::thompson;
|
||||
use regex_automata::util::syntax::Config;
|
||||
use regex_automata::{Anchored, Input, MatchError, MatchKind};
|
||||
use string_offset::CharOffset;
|
||||
use sum_tree::SumTree;
|
||||
|
||||
use string_offset::CharOffset;
|
||||
|
||||
use super::buffer::Buffer;
|
||||
use super::cursor::BufferCursor;
|
||||
use super::text::{BufferSummary, BufferText};
|
||||
use crate::search::RestorableSearchResults;
|
||||
|
||||
use super::{
|
||||
buffer::Buffer,
|
||||
cursor::BufferCursor,
|
||||
text::{BufferSummary, BufferText},
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "find_tests.rs"]
|
||||
mod tests;
|
||||
@@ -199,7 +192,7 @@ impl Engine {
|
||||
buffer: &SumTree<BufferText>,
|
||||
buffer_offset: CharOffset,
|
||||
) -> anyhow::Result<Vec<Match>> {
|
||||
galaxyui::r#async::block_on(self.find(buffer, buffer_offset))
|
||||
galaxyui_core::r#async::block_on(self.find(buffer, buffer_offset))
|
||||
}
|
||||
|
||||
/// Find all matches for this pattern in the given slice of content.
|
||||
@@ -253,7 +246,7 @@ impl Engine {
|
||||
log::warn!("Forward DFA found a match end, but reverse DFA did not find a start");
|
||||
}
|
||||
|
||||
// Because we seeked to the left of the match end, move to the next item after the
|
||||
// Because we sought to the left of the match end, move to the next item after the
|
||||
// match to prevent an infinite loop.
|
||||
buffer_cursor.next_char_position();
|
||||
|
||||
@@ -319,7 +312,16 @@ impl Engine {
|
||||
}
|
||||
} else if let Some(character) = cursor.char() {
|
||||
let mut bytes = [0u8; 4];
|
||||
for byte in character.encode_utf8(&mut bytes).bytes() {
|
||||
let encoded = character.encode_utf8(&mut bytes).len();
|
||||
let utf8 = &mut bytes[..encoded];
|
||||
// The reverse DFA was compiled over the reversed byte stream, so a
|
||||
// multi-byte character's bytes must be fed to it back-to-front.
|
||||
// Single-byte (ASCII) characters are unaffected, which is why the bug
|
||||
// only surfaced for non-ASCII (e.g. CJK) queries.
|
||||
if matches!(direction, SearchDirection::Reverse) {
|
||||
utf8.reverse();
|
||||
}
|
||||
for &byte in utf8.iter() {
|
||||
state = dfa
|
||||
.next_state(cache, state, byte)
|
||||
.context("Couldn't advance to next state")?;
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
use std::{iter, pin::pin, sync::Once};
|
||||
use std::iter;
|
||||
use std::pin::pin;
|
||||
use std::sync::Once;
|
||||
|
||||
use futures_lite::future;
|
||||
use galaxyui::App;
|
||||
use itertools::Itertools;
|
||||
use rangemap::RangeSet;
|
||||
use sum_tree::SumTree;
|
||||
|
||||
use crate::content::{
|
||||
buffer::Buffer,
|
||||
cursor::BufferSumTree,
|
||||
text::{BufferBlockStyle, BufferText, IndentBehavior},
|
||||
};
|
||||
use string_offset::CharOffset;
|
||||
use sum_tree::SumTree;
|
||||
use galaxyui_core::App;
|
||||
|
||||
use super::{Engine, Match, SearchConfig};
|
||||
use crate::content::buffer::Buffer;
|
||||
use crate::content::cursor::BufferSumTree;
|
||||
use crate::content::text::{BufferBlockStyle, BufferText, IndentBehavior};
|
||||
|
||||
#[test]
|
||||
fn test_search_inline_styles() {
|
||||
@@ -140,6 +140,74 @@ fn test_end_of_buffer() {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_non_ascii() {
|
||||
App::test((), |mut app| async move {
|
||||
let (buffer, _selection) = Buffer::mock_from_markdown(
|
||||
"你好aaaaa\n再見你好",
|
||||
None,
|
||||
Box::new(|_, _| IndentBehavior::Ignore),
|
||||
&mut app,
|
||||
);
|
||||
buffer.read(&app, |buffer, _| {
|
||||
// ASCII literal search works regardless of the bug.
|
||||
assert_matches(
|
||||
buffer,
|
||||
&SearchConfig::new("a"),
|
||||
[
|
||||
(3, 4, "a"),
|
||||
(4, 5, "a"),
|
||||
(5, 6, "a"),
|
||||
(6, 7, "a"),
|
||||
(7, 8, "a"),
|
||||
],
|
||||
);
|
||||
|
||||
// Multi-byte literal search must find every occurrence. Each CJK
|
||||
// character is one `CharOffset` but several UTF-8 bytes; the reverse
|
||||
// DFA pass that locates a match's start has to consume those bytes in
|
||||
// reverse order, which is what this regression guards.
|
||||
assert_matches(
|
||||
buffer,
|
||||
&SearchConfig::new("你好"),
|
||||
[(1, 3, "你好"), (11, 13, "你好")],
|
||||
);
|
||||
|
||||
// A multi-byte query mixing scripts also works.
|
||||
assert_matches(buffer, &SearchConfig::new("好a"), [(2, 4, "好a")]);
|
||||
|
||||
// Regex over multi-byte content keeps working.
|
||||
assert_matches(
|
||||
buffer,
|
||||
&SearchConfig::regex("你."),
|
||||
[(1, 3, "你好"), (11, 13, "你好")],
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_non_ascii_case_insensitive() {
|
||||
App::test((), |mut app| async move {
|
||||
let (buffer, _selection) = Buffer::mock_from_markdown(
|
||||
"CAFÉ café",
|
||||
None,
|
||||
Box::new(|_, _| IndentBehavior::Ignore),
|
||||
&mut app,
|
||||
);
|
||||
buffer.read(&app, |buffer, _| {
|
||||
// Case-insensitive matching over multi-byte characters (`É`/`é` are
|
||||
// two UTF-8 bytes) must find both the upper- and lower-case forms,
|
||||
// exercising the reverse-DFA byte order on case-folded input.
|
||||
assert_matches(
|
||||
buffer,
|
||||
&SearchConfig::new("café").with_case_sensitive(false),
|
||||
[(1, 5, "CAFÉ"), (6, 10, "café")],
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_word_boundaries() {
|
||||
App::test((), |mut app| async move {
|
||||
|
||||
@@ -4,13 +4,14 @@ use std::ops::Range;
|
||||
use galaxyui::{AppContext, Entity, ModelContext, ModelHandle};
|
||||
use rangemap::RangeSet;
|
||||
use string_offset::CharOffset;
|
||||
|
||||
use crate::content::edit::EditDelta;
|
||||
use crate::content::selection_model::BufferSelectionModel;
|
||||
use crate::content::version::BufferVersion;
|
||||
use crate::content::{buffer::Buffer, text::LineCount};
|
||||
use galaxyui_core::{AppContext, Entity, ModelContext, ModelHandle};
|
||||
|
||||
use super::anchor::{Anchor, AnchorSide};
|
||||
use crate::content::buffer::Buffer;
|
||||
use crate::content::edit::EditDelta;
|
||||
use crate::content::selection_model::BufferSelectionModel;
|
||||
use crate::content::text::LineCount;
|
||||
use crate::content::version::BufferVersion;
|
||||
|
||||
/// A model that tracks hidden line ranges independently of the buffer content.
|
||||
/// This allows multiple editors to have different hidden line states for the same buffer.
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::fmt::Write;
|
||||
use std::ops::Range;
|
||||
use std::{io, iter};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use galaxyui::text::point::Point;
|
||||
use galaxyui::{AppContext, ModelContext, ModelHandle};
|
||||
use html5ever::serialize;
|
||||
use html5ever::{
|
||||
QualName,
|
||||
serialize::{Serialize, Serializer, TraversalScope},
|
||||
};
|
||||
use html5ever::serialize::{Serialize, Serializer, TraversalScope};
|
||||
use html5ever::{QualName, serialize};
|
||||
use itertools::Itertools;
|
||||
use markdown_parser::{
|
||||
CodeBlockText, FormattedIndentTextInline, FormattedTableAlignment, FormattedTaskList,
|
||||
@@ -13,30 +13,23 @@ use markdown_parser::{
|
||||
FormattedTextLine, OrderedFormattedIndentTextInline,
|
||||
};
|
||||
use markup5ever::ns;
|
||||
use std::collections::VecDeque;
|
||||
use std::fmt::Write;
|
||||
use std::iter;
|
||||
use std::{io, ops::Range};
|
||||
|
||||
use galaxyui::elements::{ListIndentLevel, ListNumbering};
|
||||
use string_offset::CharOffset;
|
||||
|
||||
use crate::content::anchor::AnchorSide;
|
||||
use crate::content::selection_model::BufferSelectionModel;
|
||||
use crate::content::version::BufferVersion;
|
||||
use galaxyui_core::elements::{ListIndentLevel, ListNumbering};
|
||||
use galaxyui_core::text::point::Point;
|
||||
use galaxyui_core::{AppContext, ModelContext, ModelHandle};
|
||||
|
||||
use super::buffer::{
|
||||
ActionWithSelectionDelta, EditOrigin, EditResult, StyledBufferBlocks, StyledBufferRun,
|
||||
ActionWithSelectionDelta, Buffer, EditOrigin, EditResult, StyledBufferBlock,
|
||||
StyledBufferBlocks, StyledBufferRun,
|
||||
};
|
||||
use super::core::{CoreEditorAction, CoreEditorActionType};
|
||||
use super::text::{
|
||||
BlockHeaderSize, BlockType, BufferBlockItem, BufferBlockStyle, FormattedTable,
|
||||
TABLE_BLOCK_MARKDOWN_LANG,
|
||||
};
|
||||
use super::{
|
||||
buffer::{Buffer, StyledBufferBlock},
|
||||
text::TextStylesWithMetadata,
|
||||
TABLE_BLOCK_MARKDOWN_LANG, TextStylesWithMetadata,
|
||||
};
|
||||
use crate::content::anchor::AnchorSide;
|
||||
use crate::content::selection_model::BufferSelectionModel;
|
||||
use crate::content::version::BufferVersion;
|
||||
|
||||
/// A Markdown format to serialize a [`Buffer`] into.
|
||||
#[derive(Clone, Copy)]
|
||||
@@ -559,9 +552,10 @@ impl TextStylesWithMetadata {
|
||||
}
|
||||
}
|
||||
|
||||
use crate::content::buffer::{BufferEvent, ShouldAutoscroll, ToBufferCharOffset};
|
||||
use markdown_parser::FormattedTextDelta;
|
||||
|
||||
use crate::content::buffer::{BufferEvent, ShouldAutoscroll, ToBufferCharOffset};
|
||||
|
||||
impl Buffer {
|
||||
pub(super) fn replace_with_formatted_text(
|
||||
&mut self,
|
||||
|
||||
@@ -5,15 +5,12 @@ use markdown_parser::{compute_formatted_text_delta, parse_markdown};
|
||||
use serde_yaml::Value;
|
||||
use string_offset::CharOffset;
|
||||
use vec1::Vec1;
|
||||
|
||||
use crate::content::{
|
||||
buffer::{
|
||||
Buffer, BufferEditAction, EditOrigin, StyledBlockBoundaryBehavior, tests::TestEmbeddedItem,
|
||||
},
|
||||
text::{IndentBehavior, TABLE_BLOCK_MARKDOWN_LANG},
|
||||
};
|
||||
use galaxyui_core::{App, ReadModel};
|
||||
|
||||
use super::MarkdownStyle;
|
||||
use crate::content::buffer::tests::TestEmbeddedItem;
|
||||
use crate::content::buffer::{Buffer, BufferEditAction, EditOrigin, StyledBlockBoundaryBehavior};
|
||||
use crate::content::text::{IndentBehavior, TABLE_BLOCK_MARKDOWN_LANG};
|
||||
|
||||
#[test]
|
||||
fn test_export_normalizes_code_languages() {
|
||||
@@ -361,6 +358,122 @@ fn test_table_markdown_export_escapes_pipe_characters() {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_url_link_display_text_round_trip_is_stable() {
|
||||
App::test((), |mut app| async move {
|
||||
let original =
|
||||
"[https://example.com/index.html#section](https://example.com/index.html#section)";
|
||||
// After the first save, `.` and `#` in the display text are escaped.
|
||||
// The URL in `(...)` is written verbatim — no escaping.
|
||||
let expected_escaped = "[https://example\\.com/index\\.html\\#section](https://example.com/index.html#section)";
|
||||
|
||||
let (buffer, _) = Buffer::mock_from_markdown(
|
||||
original,
|
||||
None,
|
||||
Box::new(|_, _| IndentBehavior::Ignore),
|
||||
&mut app,
|
||||
);
|
||||
let after_first = app.read_model(&buffer, |buffer, _| buffer.markdown());
|
||||
assert_eq!(
|
||||
after_first, expected_escaped,
|
||||
"first save should escape special chars in display text"
|
||||
);
|
||||
|
||||
let (buffer2, _) = Buffer::mock_from_markdown(
|
||||
&after_first,
|
||||
None,
|
||||
Box::new(|_, _| IndentBehavior::Ignore),
|
||||
&mut app,
|
||||
);
|
||||
let after_second = app.read_model(&buffer2, |buffer, _| buffer.markdown());
|
||||
assert_eq!(
|
||||
after_second, expected_escaped,
|
||||
"second round-trip should be stable"
|
||||
);
|
||||
|
||||
let (buffer3, _) = Buffer::mock_from_markdown(
|
||||
&after_second,
|
||||
None,
|
||||
Box::new(|_, _| IndentBehavior::Ignore),
|
||||
&mut app,
|
||||
);
|
||||
let after_third = app.read_model(&buffer3, |buffer, _| buffer.markdown());
|
||||
assert_eq!(
|
||||
after_third, expected_escaped,
|
||||
"third round-trip should be stable"
|
||||
);
|
||||
|
||||
// Plain text should be the clean, unescaped URL — no backslashes.
|
||||
let plain_text = app.read_model(&buffer3, |buffer, _| buffer.text().as_str().to_string());
|
||||
assert_eq!(plain_text, "https://example.com/index.html#section");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_markdown_escapes_punctuation() {
|
||||
App::test((), |mut app| async move {
|
||||
// markdown() escapes special chars.
|
||||
let markdown = "Here's a markdown comment.\n";
|
||||
let (buffer, _) = Buffer::mock_from_markdown(
|
||||
markdown,
|
||||
None,
|
||||
Box::new(|_, _| IndentBehavior::Ignore),
|
||||
&mut app,
|
||||
);
|
||||
let escaped = app.read_model(&buffer, |buffer, _| buffer.markdown());
|
||||
assert!(
|
||||
escaped.contains("\\."),
|
||||
"expected escaped periods, got: {escaped}"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_markdown_unescaped_does_not_escape_punctuation() {
|
||||
App::test((), |mut app| async move {
|
||||
// markdown_unescaped() should not add backslashes before periods.
|
||||
let markdown = "Here's a markdown comment.\n";
|
||||
let (buffer, _) = Buffer::mock_from_markdown(
|
||||
markdown,
|
||||
None,
|
||||
Box::new(|_, _| IndentBehavior::Ignore),
|
||||
&mut app,
|
||||
);
|
||||
let unescaped = app.read_model(&buffer, |buffer, _| buffer.markdown_unescaped());
|
||||
assert!(
|
||||
!unescaped.contains("\\."),
|
||||
"expected no escaped periods, got: {unescaped}"
|
||||
);
|
||||
assert!(
|
||||
unescaped.contains("comment."),
|
||||
"expected unescaped period, got: {unescaped}"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_markdown_unescaped_preserves_urls() {
|
||||
App::test((), |mut app| async move {
|
||||
// markdown_unescaped() should not escape characters inside URLs.
|
||||
let markdown = "Check out https://www.example.com/path\n";
|
||||
let (buffer, _) = Buffer::mock_from_markdown(
|
||||
markdown,
|
||||
None,
|
||||
Box::new(|_, _| IndentBehavior::Ignore),
|
||||
&mut app,
|
||||
);
|
||||
let unescaped = app.read_model(&buffer, |buffer, _| buffer.markdown_unescaped());
|
||||
assert!(
|
||||
!unescaped.contains("\\/"),
|
||||
"expected no escaped slashes, got: {unescaped}"
|
||||
);
|
||||
assert!(
|
||||
unescaped.contains("https://www.example.com/path"),
|
||||
"expected URL preserved, got: {unescaped}"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_image_with_content_html_serialization() {
|
||||
App::test((), |mut app| async move {
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
use std::{
|
||||
hash::{DefaultHasher, Hash, Hasher},
|
||||
sync::Arc,
|
||||
};
|
||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||
use std::sync::Arc;
|
||||
|
||||
use bytes::Bytes;
|
||||
use galaxyui::{
|
||||
AppContext, SingletonEntity,
|
||||
assets::asset_cache::{AssetCache, AssetSource, AssetState, AsyncAssetId, AsyncAssetType},
|
||||
image_cache::ImageType,
|
||||
units::{IntoPixels, Pixels},
|
||||
use galaxyui_core::assets::asset_cache::{
|
||||
AssetCache, AssetSource, AssetState, AsyncAssetId, AsyncAssetType,
|
||||
};
|
||||
use mermaid_to_svg::MermaidTheme;
|
||||
use galaxyui_core::image_cache::ImageType;
|
||||
use galaxyui_core::units::{IntoPixels, Pixels};
|
||||
use galaxyui_core::{AppContext, SingletonEntity};
|
||||
|
||||
use crate::render::{
|
||||
layout::TextLayout,
|
||||
model::{BlockSpacing, ImageBlockConfig},
|
||||
};
|
||||
use crate::render::layout::TextLayout;
|
||||
use crate::render::model::{BlockSpacing, ImageBlockConfig};
|
||||
|
||||
const DEFAULT_MERMAID_HEIGHT_LINE_MULTIPLIER: f32 = 10.0;
|
||||
const FAILED_MERMAID_HEIGHT_LINE_MULTIPLIER: f32 = 2.0;
|
||||
|
||||
struct MermaidDiagramAsset;
|
||||
|
||||
@@ -27,7 +23,7 @@ pub fn mermaid_asset_source(source: &str) -> AssetSource {
|
||||
let source = source.to_string();
|
||||
let mut hasher = DefaultHasher::new();
|
||||
source.hash(&mut hasher);
|
||||
let id = format!("light:{:x}", hasher.finish());
|
||||
let id = format!("configured:{:x}", hasher.finish());
|
||||
let fetch_source = source.clone();
|
||||
|
||||
AssetSource::Async {
|
||||
@@ -35,7 +31,7 @@ pub fn mermaid_asset_source(source: &str) -> AssetSource {
|
||||
fetch: Arc::new(move || {
|
||||
let source = fetch_source.clone();
|
||||
Box::pin(async move {
|
||||
mermaid_to_svg::render_mermaid_to_svg(&source, Some(&MermaidTheme::light()))
|
||||
mermaid_to_svg::render_mermaid_to_svg(&source, None)
|
||||
.map(|svg| Bytes::from(svg.into_bytes()))
|
||||
.map_err(Into::into)
|
||||
})
|
||||
@@ -50,22 +46,42 @@ pub fn mermaid_diagram_layout(
|
||||
app: &AppContext,
|
||||
) -> (AssetSource, ImageBlockConfig) {
|
||||
let asset_source = mermaid_asset_source(source);
|
||||
let max_width = layout.max_width() - spacing.x_axis_offset();
|
||||
let default_height = layout.rich_text_styles().base_line_height()
|
||||
* DEFAULT_MERMAID_HEIGHT_LINE_MULTIPLIER.into_pixels();
|
||||
let (width, height) =
|
||||
mermaid_diagram_size(&asset_source, max_width, app).unwrap_or((max_width, default_height));
|
||||
let config = mermaid_diagram_config(&asset_source, layout, spacing, app);
|
||||
|
||||
(
|
||||
asset_source,
|
||||
ImageBlockConfig {
|
||||
width,
|
||||
height,
|
||||
spacing,
|
||||
},
|
||||
)
|
||||
(asset_source, config)
|
||||
}
|
||||
|
||||
fn mermaid_diagram_config(
|
||||
asset_source: &AssetSource,
|
||||
layout: &TextLayout,
|
||||
spacing: BlockSpacing,
|
||||
app: &AppContext,
|
||||
) -> ImageBlockConfig {
|
||||
let max_width = layout.max_width() - spacing.x_axis_offset();
|
||||
let (width, height) = mermaid_diagram_size(asset_source, max_width, app).unwrap_or_else(|| {
|
||||
let height = layout.rich_text_styles().base_line_height()
|
||||
* mermaid_diagram_fallback_height_line_multiplier(asset_source, app).into_pixels();
|
||||
(max_width, height)
|
||||
});
|
||||
ImageBlockConfig {
|
||||
width,
|
||||
height,
|
||||
spacing,
|
||||
}
|
||||
}
|
||||
|
||||
fn mermaid_diagram_fallback_height_line_multiplier(
|
||||
asset_source: &AssetSource,
|
||||
app: &AppContext,
|
||||
) -> f32 {
|
||||
let asset_cache = AssetCache::as_ref(app);
|
||||
match asset_cache.load_asset::<ImageType>(asset_source.clone()) {
|
||||
AssetState::FailedToLoad(_) => FAILED_MERMAID_HEIGHT_LINE_MULTIPLIER,
|
||||
AssetState::Loading { .. } | AssetState::Loaded { .. } | AssetState::Evicted => {
|
||||
DEFAULT_MERMAID_HEIGHT_LINE_MULTIPLIER
|
||||
}
|
||||
}
|
||||
}
|
||||
fn mermaid_diagram_size(
|
||||
asset_source: &AssetSource,
|
||||
max_width: Pixels,
|
||||
@@ -85,7 +101,11 @@ fn mermaid_diagram_size(
|
||||
if intrinsic_width <= 0. || intrinsic_height <= 0. {
|
||||
return None;
|
||||
}
|
||||
let width = Pixels::new(max_width.as_f32().min(intrinsic_width));
|
||||
let width = max_width;
|
||||
let height = Pixels::new(width.as_f32() * intrinsic_height / intrinsic_width);
|
||||
Some((width, height))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "mermaid_diagram_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
use galaxyui_core::assets::asset_cache::{AssetCache, AssetSource, AssetState};
|
||||
use galaxyui_core::image_cache::ImageType;
|
||||
use galaxyui_core::text_layout::LayoutCache;
|
||||
use galaxyui_core::{App, SingletonEntity};
|
||||
|
||||
use super::*;
|
||||
use crate::render::layout::TextLayout;
|
||||
use crate::render::model::test_utils::TEST_STYLES;
|
||||
|
||||
fn mermaid_block_spacing() -> BlockSpacing {
|
||||
TEST_STYLES.block_spacings.from_block_style(
|
||||
&crate::content::text::BufferBlockStyle::CodeBlock {
|
||||
code_block_type: crate::content::text::CodeBlockType::Mermaid,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn loading_mermaid_layout_uses_default_height() {
|
||||
App::test((), |app| async move {
|
||||
app.read(|ctx| {
|
||||
let source = "graph TD\nA[Start] --> B[Finish]\n";
|
||||
let layout_cache = LayoutCache::new();
|
||||
let text_layout = TextLayout::new(
|
||||
&layout_cache,
|
||||
ctx.font_cache().text_layout_system(),
|
||||
&TEST_STYLES,
|
||||
800.,
|
||||
);
|
||||
let (_asset_source, config) =
|
||||
mermaid_diagram_layout(source, &text_layout, mermaid_block_spacing(), ctx);
|
||||
let expected_height = TEST_STYLES.base_line_height()
|
||||
* DEFAULT_MERMAID_HEIGHT_LINE_MULTIPLIER.into_pixels();
|
||||
|
||||
assert!((config.height.as_f32() - expected_height.as_f32()).abs() < 0.5);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mermaid_asset_source_renders_frontmatter_formatting_directives() {
|
||||
let source = r##"---
|
||||
config:
|
||||
theme: base
|
||||
themeVariables:
|
||||
primaryColor: "#ff0000"
|
||||
fontFamily: Inter
|
||||
fontSize: 18px
|
||||
flowchart:
|
||||
curve: linear
|
||||
nodeSpacing: 80
|
||||
---
|
||||
flowchart TD
|
||||
A[Start] --> B[Done]
|
||||
"##;
|
||||
|
||||
let AssetSource::Async { fetch, .. } = mermaid_asset_source(source) else {
|
||||
panic!("expected Mermaid diagrams to be async assets");
|
||||
};
|
||||
let bytes = match futures_lite::future::block_on(fetch()) {
|
||||
Ok(bytes) => bytes,
|
||||
Err(error) => panic!("expected frontmatter directives to render: {error:#}"),
|
||||
};
|
||||
let svg = match String::from_utf8(bytes.to_vec()) {
|
||||
Ok(svg) => svg,
|
||||
Err(error) => panic!("expected Mermaid SVG to be valid UTF-8: {error}"),
|
||||
};
|
||||
|
||||
assert!(svg.contains("<svg "));
|
||||
assert!(svg.contains(r##"fill="#ff0000""##));
|
||||
assert!(svg.contains(r#"font-family="Inter""#));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_mermaid_layout_uses_compact_height() {
|
||||
App::test((), |app| async move {
|
||||
app.read(|ctx| {
|
||||
let asset_source = AssetSource::Raw {
|
||||
id: "missing-mermaid-test-asset".to_string(),
|
||||
};
|
||||
let asset_cache = AssetCache::as_ref(ctx);
|
||||
assert!(matches!(
|
||||
asset_cache.load_asset::<ImageType>(asset_source.clone()),
|
||||
AssetState::FailedToLoad(_)
|
||||
));
|
||||
|
||||
let layout_cache = LayoutCache::new();
|
||||
let text_layout = TextLayout::new(
|
||||
&layout_cache,
|
||||
ctx.font_cache().text_layout_system(),
|
||||
&TEST_STYLES,
|
||||
800.,
|
||||
);
|
||||
let config =
|
||||
mermaid_diagram_config(&asset_source, &text_layout, mermaid_block_spacing(), ctx);
|
||||
let expected_height = TEST_STYLES.base_line_height()
|
||||
* FAILED_MERMAID_HEIGHT_LINE_MULTIPLIER.into_pixels();
|
||||
|
||||
assert!((config.height.as_f32() - expected_height.as_f32()).abs() < 0.5);
|
||||
});
|
||||
})
|
||||
}
|
||||
@@ -1,12 +1,9 @@
|
||||
use string_offset::CharOffset;
|
||||
use sum_tree::{Cursor, SeekBias};
|
||||
|
||||
use super::buffer::Buffer;
|
||||
use super::text::{BlockCount, BufferBlockStyle, BufferText};
|
||||
use crate::content::text::BlockType;
|
||||
use string_offset::CharOffset;
|
||||
|
||||
use super::{
|
||||
buffer::Buffer,
|
||||
text::{BlockCount, BufferBlockStyle, BufferText},
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "outline_tests.rs"]
|
||||
@@ -82,7 +79,7 @@ impl Iterator for BlockOutlines<'_> {
|
||||
let end_count = self.count + 1;
|
||||
self.cursor.seek(&end_count, SeekBias::Left);
|
||||
|
||||
// We seeked to the start of the next block, so it's where we start on the next pass.
|
||||
// We sought to the start of the next block, so it's where we start on the next pass.
|
||||
self.count = end_count;
|
||||
Some(BlockOutline {
|
||||
start: start_offset,
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
use itertools::Itertools;
|
||||
|
||||
use crate::content::{
|
||||
buffer::Buffer,
|
||||
outline::BlockOutline,
|
||||
selection_model::BufferSelectionModel,
|
||||
text::{BlockType, BufferBlockStyle, IndentBehavior, TextStyles},
|
||||
};
|
||||
use galaxyui::App;
|
||||
use string_offset::CharOffset;
|
||||
use galaxyui_core::App;
|
||||
|
||||
use crate::content::buffer::Buffer;
|
||||
use crate::content::outline::BlockOutline;
|
||||
use crate::content::selection_model::BufferSelectionModel;
|
||||
use crate::content::text::{BlockType, BufferBlockStyle, IndentBehavior, TextStyles};
|
||||
|
||||
#[test]
|
||||
fn test_no_blocks() {
|
||||
|
||||
@@ -5,11 +5,12 @@
|
||||
use anyhow::anyhow;
|
||||
use galaxyui::text::{TextBuffer, point::Point, word_boundaries::WordBoundariesPolicy};
|
||||
use string_offset::CharOffset;
|
||||
use galaxyui_core::text::TextBuffer;
|
||||
use galaxyui_core::text::point::Point;
|
||||
use galaxyui_core::text::word_boundaries::WordBoundariesPolicy;
|
||||
|
||||
use super::{
|
||||
buffer::{Buffer, ToBufferCharOffset, ToBufferPoint},
|
||||
cursor::BufferCursor,
|
||||
};
|
||||
use super::buffer::{Buffer, ToBufferCharOffset, ToBufferPoint};
|
||||
use super::cursor::BufferCursor;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "segmentation_tests.rs"]
|
||||
|
||||
@@ -2,16 +2,14 @@ use galaxy_core::features::FeatureFlag;
|
||||
use itertools::Itertools;
|
||||
use markdown_parser::parse_markdown;
|
||||
use string_offset::CharOffset;
|
||||
use galaxyui_core::App;
|
||||
use galaxyui_core::text::TextBuffer;
|
||||
use galaxyui_core::text::point::Point;
|
||||
use galaxyui_core::text::word_boundaries::WordBoundariesPolicy;
|
||||
|
||||
use crate::content::{
|
||||
buffer::{Buffer, EditOrigin},
|
||||
selection_model::BufferSelectionModel,
|
||||
text::IndentBehavior,
|
||||
};
|
||||
use galaxyui::{
|
||||
App,
|
||||
text::{TextBuffer, point::Point, word_boundaries::WordBoundariesPolicy},
|
||||
};
|
||||
use crate::content::buffer::{Buffer, EditOrigin};
|
||||
use crate::content::selection_model::BufferSelectionModel;
|
||||
use crate::content::text::IndentBehavior;
|
||||
|
||||
#[test]
|
||||
fn test_forward_iteration() {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use vec1::Vec1;
|
||||
|
||||
use string_offset::CharOffset;
|
||||
use vec1::Vec1;
|
||||
|
||||
use super::anchor::{Anchor, AnchorSide, Anchors};
|
||||
|
||||
|
||||
@@ -4,13 +4,12 @@ use galaxyui::{AppContext, Entity, ModelHandle};
|
||||
use itertools::Itertools;
|
||||
use string_offset::CharOffset;
|
||||
use vec1::{Vec1, vec1};
|
||||
use galaxyui_core::{AppContext, Entity, ModelHandle};
|
||||
|
||||
use crate::content::{
|
||||
anchor::{Anchor, AnchorSide, AnchorUpdate, Anchors},
|
||||
buffer::{Buffer, SelectionOffsets, ToBufferPoint},
|
||||
selection::{Selection, SelectionSet},
|
||||
text::{BlockType, TextStylesWithMetadata},
|
||||
};
|
||||
use crate::content::anchor::{Anchor, AnchorSide, AnchorUpdate, Anchors};
|
||||
use crate::content::buffer::{Buffer, SelectionOffsets, ToBufferPoint};
|
||||
use crate::content::selection::{Selection, SelectionSet};
|
||||
use crate::content::text::{BlockType, TextStylesWithMetadata};
|
||||
|
||||
/// A snapshot of the selection state. This includes all data reported by [`BufferEvent::SelectionChanged`].
|
||||
#[derive(PartialEq, Eq)]
|
||||
@@ -309,7 +308,7 @@ impl BufferSelectionModel {
|
||||
}
|
||||
|
||||
/// Validate the buffer content with this selection model's anchors.
|
||||
pub fn validate_buffer(&self, ctx: &impl galaxyui::ModelAsRef) {
|
||||
pub fn validate_buffer(&self, ctx: &impl galaxyui_core::ModelAsRef) {
|
||||
self.buffer.as_ref(ctx).validate(&self.anchors);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
use crate::render::model::{
|
||||
EmbeddedItem,
|
||||
table_offset_map::{TableCellOffsetMap, TableOffsetMap},
|
||||
};
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashSet;
|
||||
use std::fmt::{self, Display};
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::ops::{Add, AddAssign, BitXor, BitXorAssign, Range, Sub, SubAssign};
|
||||
use std::sync::{Arc, OnceLock};
|
||||
|
||||
use arrayvec::ArrayString;
|
||||
use enum_iterator::Sequence;
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
@@ -14,13 +17,13 @@ use galaxyui::{
|
||||
};
|
||||
use lazy_static::lazy_static;
|
||||
pub use markdown_parser::markdown_parser::TABLE_BLOCK_MARKDOWN_LANG;
|
||||
use markdown_parser::markdown_parser::{
|
||||
CODE_BLOCK_DEFAULT_MARKDOWN_LANG, EMBED_BLOCK_MARKDOWN_LANG, RUNNABLE_BLOCK_MARKDOWN_LANG,
|
||||
};
|
||||
use markdown_parser::weight::CustomWeight;
|
||||
use markdown_parser::{
|
||||
CodeBlockText, FormattedImage, FormattedTextLine, FormattedTextStyles, Hyperlink,
|
||||
markdown_parser::{
|
||||
CODE_BLOCK_DEFAULT_MARKDOWN_LANG, EMBED_BLOCK_MARKDOWN_LANG, RUNNABLE_BLOCK_MARKDOWN_LANG,
|
||||
},
|
||||
parse_markdown,
|
||||
weight::CustomWeight,
|
||||
};
|
||||
pub use markdown_parser::{
|
||||
FormattedTable, FormattedTableAlignment, FormattedTextFragment, FormattedTextInline,
|
||||
@@ -28,18 +31,19 @@ pub use markdown_parser::{
|
||||
use pathfinder_color::ColorU;
|
||||
use rand::Rng;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
collections::HashSet,
|
||||
fmt::{self, Display},
|
||||
hash::{Hash, Hasher},
|
||||
ops::{Add, AddAssign, BitXor, BitXorAssign, Range, Sub, SubAssign},
|
||||
sync::{Arc, OnceLock},
|
||||
};
|
||||
use string_offset::{ByteOffset, CharOffset, impl_offset};
|
||||
use sum_tree::{Cursor, SeekBias, SumTree};
|
||||
use galaxyui_core::AppContext;
|
||||
use galaxyui_core::elements::ListIndentLevel;
|
||||
use galaxyui_core::fonts::{Properties, Style, Weight};
|
||||
use galaxyui_core::text::BlockHeaderSize as HeaderSize;
|
||||
use galaxyui_core::text::point::Point;
|
||||
|
||||
use super::{buffer::Buffer, core::CursorType, markdown::MarkdownStyle};
|
||||
use super::buffer::Buffer;
|
||||
use super::core::CursorType;
|
||||
use super::markdown::MarkdownStyle;
|
||||
use crate::render::model::EmbeddedItem;
|
||||
use crate::render::model::table_offset_map::{TableCellOffsetMap, TableOffsetMap};
|
||||
|
||||
/// Collect the plain text from a `FormattedTextInline` (a slice of fragments).
|
||||
pub fn inline_to_text(inline: &[FormattedTextFragment]) -> String {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use galaxyui::fonts::Weight;
|
||||
use markdown_parser::CodeBlockText;
|
||||
|
||||
use markdown_parser::{CodeBlockText, FormattedTable};
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use markdown_parser::FormattedTable;
|
||||
use galaxyui_core::fonts::Weight;
|
||||
|
||||
use super::{
|
||||
BufferBlockItem, BufferTextStyle, CodeBlockType, MarkdownStyle, TextStyles,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use std::{collections::VecDeque, time::Duration};
|
||||
use std::collections::VecDeque;
|
||||
use std::time::Duration;
|
||||
|
||||
use galaxy_util::content_version::ContentVersion;
|
||||
use instant::Instant;
|
||||
|
||||
use crate::render::model::RenderedSelectionSet;
|
||||
|
||||
use super::core::{CoreEditorAction, ReplacementRange};
|
||||
use crate::render::model::RenderedSelectionSet;
|
||||
|
||||
/// Threshold to separate two non-atomic undo items.
|
||||
const UNDO_REDO_TIMER: Duration = Duration::from_millis(500);
|
||||
@@ -338,5 +338,5 @@ impl UndoStack {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "undo_test.rs"]
|
||||
#[path = "undo_tests.rs"]
|
||||
pub mod tests;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use crate::render::model::RenderedSelection;
|
||||
|
||||
use super::*;
|
||||
use crate::render::model::RenderedSelection;
|
||||
|
||||
#[test]
|
||||
fn test_version_match_initial() {
|
||||
@@ -1,12 +1,9 @@
|
||||
use pathfinder_color::ColorU;
|
||||
use sum_tree::SumTree;
|
||||
|
||||
use crate::content::{
|
||||
cursor::BufferSumTree,
|
||||
text::{BlockLineBreakBehavior, BlockType, ColorMarker},
|
||||
};
|
||||
|
||||
use super::text::{BufferBlockStyle, BufferSummary, BufferText, MarkerDir};
|
||||
use crate::content::cursor::BufferSumTree;
|
||||
use crate::content::text::{BlockLineBreakBehavior, BlockType, ColorMarker};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "validation_tests.rs"]
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
use crate::content::{
|
||||
cursor::BufferSumTree,
|
||||
text::{
|
||||
BlockHeaderSize, BufferBlockItem, BufferBlockStyle, BufferText, BufferTextStyle, MarkerDir,
|
||||
},
|
||||
};
|
||||
use galaxyui::elements::ListIndentLevel;
|
||||
use sum_tree::SumTree;
|
||||
use galaxyui_core::elements::ListIndentLevel;
|
||||
|
||||
use crate::content::cursor::BufferSumTree;
|
||||
use crate::content::text::{
|
||||
BlockHeaderSize, BufferBlockItem, BufferBlockStyle, BufferText, BufferTextStyle, MarkerDir,
|
||||
};
|
||||
|
||||
#[test]
|
||||
#[should_panic(
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use galaxyui::ModelContext;
|
||||
use galaxyui_core::ModelContext;
|
||||
|
||||
use crate::content::{buffer::BufferSnapshot, edit::PreciseDelta, version::BufferVersion};
|
||||
use crate::content::buffer::BufferSnapshot;
|
||||
use crate::content::edit::PreciseDelta;
|
||||
use crate::content::version::BufferVersion;
|
||||
|
||||
pub trait DecorationLayer {
|
||||
fn update_internal_state_with_delta(
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
//! Interoperability traits for interaction between the generic rendering/content layers and parent
|
||||
//! editor layers.
|
||||
|
||||
use std::{any::Any, cell::Ref, ops::Range};
|
||||
use std::any::Any;
|
||||
use std::cell::Ref;
|
||||
use std::ops::Range;
|
||||
|
||||
use galaxyui::{
|
||||
Action, AppContext, Element, TypedActionView, View, elements::Border,
|
||||
text_layout::PaintStyleOverride,
|
||||
};
|
||||
use num_traits::SaturatingSub;
|
||||
use pathfinder_color::ColorU;
|
||||
use rangemap::{RangeMap, RangeSet};
|
||||
|
||||
use crate::{content::version::BufferVersion, render::element::RichTextAction};
|
||||
use string_offset::CharOffset;
|
||||
use galaxyui_core::elements::Border;
|
||||
use galaxyui_core::text_layout::PaintStyleOverride;
|
||||
use galaxyui_core::{Action, AppContext, Element, TypedActionView, View};
|
||||
|
||||
use crate::content::version::BufferVersion;
|
||||
use crate::render::element::RichTextAction;
|
||||
|
||||
/// Interface between a `RichTextElement` and its containing editor view.
|
||||
pub trait EditorView
|
||||
|
||||
+37
-18
@@ -5,24 +5,24 @@ use galaxyui::{
|
||||
};
|
||||
use itertools::{Either, Itertools};
|
||||
use line_ending::LineEnding;
|
||||
use vec1::{Vec1, vec1};
|
||||
|
||||
use crate::{
|
||||
content::{
|
||||
anchor::Anchor,
|
||||
buffer::{
|
||||
AutoScrollBehavior, Buffer, BufferEditAction, BufferSelectAction, EditOrigin,
|
||||
InitialBufferState, SelectionOffsets, ShouldAutoscroll, ToBufferCharOffset,
|
||||
},
|
||||
selection_model::BufferSelectionModel,
|
||||
text::{BlockType, BufferBlockItem, BufferBlockStyle, CodeBlockType, TextStyles},
|
||||
version::BufferVersion,
|
||||
},
|
||||
render::model::RenderState,
|
||||
selection::{SelectionMode, SelectionModel, TextDirection, TextUnit},
|
||||
};
|
||||
use galaxyui::elements::ListIndentLevel;
|
||||
use string_offset::{ByteOffset, CharOffset};
|
||||
use vec1::{Vec1, vec1};
|
||||
use galaxyui_core::clipboard::ClipboardContent;
|
||||
use galaxyui_core::elements::ListIndentLevel;
|
||||
use galaxyui_core::{AppContext, Entity, ModelAsRef, ModelContext, ModelHandle};
|
||||
|
||||
use crate::content::anchor::Anchor;
|
||||
use crate::content::buffer::{
|
||||
AutoScrollBehavior, Buffer, BufferEditAction, BufferSelectAction, EditOrigin,
|
||||
InitialBufferState, SelectionOffsets, ShouldAutoscroll, ToBufferCharOffset,
|
||||
};
|
||||
use crate::content::selection_model::BufferSelectionModel;
|
||||
use crate::content::text::{
|
||||
BlockType, BufferBlockItem, BufferBlockStyle, CodeBlockType, TextStyles,
|
||||
};
|
||||
use crate::content::version::BufferVersion;
|
||||
use crate::render::model::RenderState;
|
||||
use crate::selection::{SelectionMode, SelectionModel, TextDirection, TextUnit};
|
||||
|
||||
/// A wrapper for a buffer that provides access to its internal update_content method.
|
||||
/// It's important this is only returned from `CoreEditorModel::update_content` method
|
||||
@@ -917,10 +917,29 @@ pub trait RichTextEditorModel: CoreEditorModel {
|
||||
self.validate(ctx);
|
||||
}
|
||||
|
||||
fn reset_with_ipynb(&mut self, ipynb: &str, ctx: &mut ModelContext<Self::T>) {
|
||||
let state = InitialBufferState::ipynb(ipynb);
|
||||
|
||||
self.update_content(
|
||||
|mut content, ctx| {
|
||||
content.buffer().reset_undo_stack();
|
||||
content.apply_edit(
|
||||
BufferEditAction::ReplaceWith(state),
|
||||
EditOrigin::SystemEdit,
|
||||
self.buffer_selection_model().clone(),
|
||||
ctx,
|
||||
);
|
||||
},
|
||||
ctx,
|
||||
);
|
||||
self.validate(ctx);
|
||||
}
|
||||
|
||||
fn update_to_new_markdown(&mut self, markdown: &str, ctx: &mut ModelContext<Self::T>) {
|
||||
use crate::content::buffer::StyledBlockBoundaryBehavior;
|
||||
use markdown_parser::{compute_formatted_text_delta, parse_markdown};
|
||||
|
||||
use crate::content::buffer::StyledBlockBoundaryBehavior;
|
||||
|
||||
// Try to obtain the current formatted-text view from the buffer and
|
||||
// compute both the `FormattedText` delta and the common prefix length in
|
||||
// characters between the old and new markdown.
|
||||
|
||||
@@ -37,12 +37,10 @@
|
||||
//! * [`AnyMultilineString::to_line_ending`] or [`AnyMultilineString::into_line_ending`] to convert
|
||||
//! to a line ending known at runtime
|
||||
|
||||
use std::{
|
||||
borrow::{Borrow, Cow},
|
||||
fmt,
|
||||
marker::PhantomData,
|
||||
ops::Deref,
|
||||
};
|
||||
use std::borrow::{Borrow, Cow};
|
||||
use std::fmt;
|
||||
use std::marker::PhantomData;
|
||||
use std::ops::Deref;
|
||||
|
||||
use itertools::Itertools as _;
|
||||
use line_ending::LineEnding;
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
// it's used in the implementation of `infer_line_ending`.
|
||||
#![allow(clippy::disallowed_methods)]
|
||||
|
||||
use super::*;
|
||||
use galaxy_core::platform::SessionPlatform;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_infer_line_ending_empty_file() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -1,22 +1,16 @@
|
||||
use galaxyui::{
|
||||
AppContext, Element, SizeConstraint,
|
||||
elements::{
|
||||
Align, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Empty, Flex, Icon,
|
||||
ParentElement, Radius, Shrinkable, Text,
|
||||
},
|
||||
geometry::vector::vec2f,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
editor::EmbeddedItemModel,
|
||||
extract_block,
|
||||
render::{
|
||||
element::paint::{CursorData, CursorDisplayType},
|
||||
model::{BlockItem, RichTextStyles, viewport::ViewportItem},
|
||||
},
|
||||
use galaxyui_core::elements::{
|
||||
Align, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Empty, Flex, Icon,
|
||||
ParentElement, Radius, Shrinkable, Text,
|
||||
};
|
||||
use galaxyui_core::geometry::vector::vec2f;
|
||||
use galaxyui_core::{AppContext, Element, SizeConstraint};
|
||||
|
||||
use super::RenderableBlock;
|
||||
use crate::editor::EmbeddedItemModel;
|
||||
use crate::extract_block;
|
||||
use crate::render::element::paint::{CursorData, CursorDisplayType};
|
||||
use crate::render::model::viewport::ViewportItem;
|
||||
use crate::render::model::{BlockItem, RichTextStyles};
|
||||
|
||||
pub struct RenderableBrokenEmbedding {
|
||||
row: Box<dyn Element>,
|
||||
@@ -79,8 +73,8 @@ impl RenderableBlock for RenderableBrokenEmbedding {
|
||||
fn layout(
|
||||
&mut self,
|
||||
model: &crate::render::model::RenderState,
|
||||
ctx: &mut galaxyui::LayoutContext,
|
||||
app: &galaxyui::AppContext,
|
||||
ctx: &mut galaxyui_core::LayoutContext,
|
||||
app: &galaxyui_core::AppContext,
|
||||
) {
|
||||
self.row.layout(
|
||||
SizeConstraint::strict(vec2f(
|
||||
@@ -104,7 +98,7 @@ impl RenderableBlock for RenderableBrokenEmbedding {
|
||||
&mut self,
|
||||
model: &crate::render::model::RenderState,
|
||||
ctx: &mut super::RenderContext,
|
||||
app: &galaxyui::AppContext,
|
||||
app: &galaxyui_core::AppContext,
|
||||
) {
|
||||
let content = model.content();
|
||||
let broken_link = extract_block!(self.viewport_item, content, (block, BlockItem::Embedded(item)) => block.embedded(item));
|
||||
@@ -156,21 +150,25 @@ impl RenderableBlock for RenderableBrokenEmbedding {
|
||||
|
||||
ctx.paint
|
||||
.scene
|
||||
.start_layer(galaxyui::ClipBounds::ActiveLayer);
|
||||
.start_layer(galaxyui_core::ClipBounds::ActiveLayer);
|
||||
self.row
|
||||
.paint(ctx.content_to_screen(content_origin), ctx.paint, app);
|
||||
ctx.paint.scene.stop_layer();
|
||||
}
|
||||
|
||||
fn after_layout(&mut self, ctx: &mut galaxyui::AfterLayoutContext, app: &galaxyui::AppContext) {
|
||||
fn after_layout(
|
||||
&mut self,
|
||||
ctx: &mut galaxyui_core::AfterLayoutContext,
|
||||
app: &galaxyui_core::AppContext,
|
||||
) {
|
||||
self.row.after_layout(ctx, app);
|
||||
}
|
||||
|
||||
fn dispatch_event(
|
||||
&mut self,
|
||||
_model: &crate::render::model::RenderState,
|
||||
event: &galaxyui::event::DispatchedEvent,
|
||||
ctx: &mut galaxyui::EventContext,
|
||||
event: &galaxyui_core::event::DispatchedEvent,
|
||||
ctx: &mut galaxyui_core::EventContext,
|
||||
app: &AppContext,
|
||||
) -> bool {
|
||||
self.row.dispatch_event(event, ctx, app)
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
use crate::{
|
||||
content::text::BufferBlockStyle,
|
||||
extract_block,
|
||||
render::{
|
||||
element::paint::CursorData,
|
||||
model::{BlockItem, RenderState, viewport::ViewportItem},
|
||||
},
|
||||
};
|
||||
|
||||
use super::{
|
||||
RenderContext, RenderableBlock,
|
||||
paragraph::paragraph_placeholder_text,
|
||||
placeholder::{self, BlockPlaceholder},
|
||||
};
|
||||
use super::paragraph::paragraph_placeholder_text;
|
||||
use super::placeholder::{self, BlockPlaceholder};
|
||||
use super::{RenderContext, RenderableBlock};
|
||||
use crate::content::text::BufferBlockStyle;
|
||||
use crate::extract_block;
|
||||
use crate::render::element::paint::CursorData;
|
||||
use crate::render::model::viewport::ViewportItem;
|
||||
use crate::render::model::{BlockItem, RenderState};
|
||||
|
||||
/// Renderable representation of invisible rich-text items. This is used for the trailing newline
|
||||
/// marker.
|
||||
@@ -37,8 +31,8 @@ impl RenderableBlock for Empty {
|
||||
fn layout(
|
||||
&mut self,
|
||||
model: &RenderState,
|
||||
ctx: &mut galaxyui::LayoutContext,
|
||||
app: &galaxyui::AppContext,
|
||||
ctx: &mut galaxyui_core::LayoutContext,
|
||||
app: &galaxyui_core::AppContext,
|
||||
) {
|
||||
self.placeholder
|
||||
.layout(&self.viewport_item, model, ctx, app, |_| {
|
||||
@@ -49,7 +43,12 @@ impl RenderableBlock for Empty {
|
||||
});
|
||||
}
|
||||
|
||||
fn paint(&mut self, model: &RenderState, ctx: &mut RenderContext, _app: &galaxyui::AppContext) {
|
||||
fn paint(
|
||||
&mut self,
|
||||
model: &RenderState,
|
||||
ctx: &mut RenderContext,
|
||||
_app: &galaxyui_core::AppContext,
|
||||
) {
|
||||
let content = model.content();
|
||||
let cursor = extract_block!(self.viewport_item, content, (block, BlockItem::TrailingNewLine(cursor)) => block.trailing_newline(cursor));
|
||||
if self.placeholder.paint(cursor.content_origin(), model, ctx) {
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
use crate::{
|
||||
content::text::{BlockHeaderSize, BufferBlockStyle},
|
||||
extract_block,
|
||||
render::model::{BlockItem, RenderState, viewport::ViewportItem},
|
||||
};
|
||||
|
||||
use super::{
|
||||
RenderContext, RenderableBlock,
|
||||
placeholder::{BlockPlaceholder, Options},
|
||||
};
|
||||
use super::placeholder::{BlockPlaceholder, Options};
|
||||
use super::{RenderContext, RenderableBlock};
|
||||
use crate::content::text::{BlockHeaderSize, BufferBlockStyle};
|
||||
use crate::extract_block;
|
||||
use crate::render::model::viewport::ViewportItem;
|
||||
use crate::render::model::{BlockItem, RenderState};
|
||||
|
||||
pub struct RenderableHeader {
|
||||
viewport_item: ViewportItem,
|
||||
@@ -31,8 +27,8 @@ impl RenderableBlock for RenderableHeader {
|
||||
fn layout(
|
||||
&mut self,
|
||||
model: &RenderState,
|
||||
ctx: &mut galaxyui::LayoutContext,
|
||||
app: &galaxyui::AppContext,
|
||||
ctx: &mut galaxyui_core::LayoutContext,
|
||||
app: &galaxyui_core::AppContext,
|
||||
) {
|
||||
self.placeholder
|
||||
.layout(&self.viewport_item, model, ctx, app, |block| {
|
||||
@@ -53,7 +49,12 @@ impl RenderableBlock for RenderableHeader {
|
||||
});
|
||||
}
|
||||
|
||||
fn paint(&mut self, model: &RenderState, ctx: &mut RenderContext, _app: &galaxyui::AppContext) {
|
||||
fn paint(
|
||||
&mut self,
|
||||
model: &RenderState,
|
||||
ctx: &mut RenderContext,
|
||||
_app: &galaxyui_core::AppContext,
|
||||
) {
|
||||
let content = model.content();
|
||||
let (paragraph, header_size) = extract_block!(
|
||||
self.viewport_item, content,
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
use crate::extract_block;
|
||||
use crate::render::model::BlockItem;
|
||||
|
||||
use super::super::model::{RenderState, viewport::ViewportItem};
|
||||
use super::{RenderContext, RenderableBlock};
|
||||
use galaxy_core::ui::appearance::Appearance;
|
||||
use galaxy_core::ui::theme::color::internal_colors;
|
||||
use galaxyui::elements::{CrossAxisAlignment, Empty, Flex, ParentElement};
|
||||
use galaxyui::{
|
||||
use galaxyui_core::elements::{Container, CrossAxisAlignment, Empty, Flex, ParentElement};
|
||||
use galaxyui_core::geometry::vector::vec2f;
|
||||
use galaxyui_core::{
|
||||
AfterLayoutContext, AppContext, Element, LayoutContext, SingletonEntity, SizeConstraint,
|
||||
elements::Container, geometry::vector::vec2f,
|
||||
};
|
||||
|
||||
use super::super::model::RenderState;
|
||||
use super::super::model::viewport::ViewportItem;
|
||||
use super::{RenderContext, RenderableBlock};
|
||||
use crate::extract_block;
|
||||
use crate::render::model::BlockItem;
|
||||
|
||||
/// A renderable block for hidden sections that renders a single- or double-line-height rectangle.
|
||||
/// This is used for BlockItem::Hidden items that need to be visually indicated.
|
||||
pub struct RenderableHiddenSection {
|
||||
@@ -71,8 +72,8 @@ impl RenderableBlock for RenderableHiddenSection {
|
||||
fn dispatch_event(
|
||||
&mut self,
|
||||
_model: &RenderState,
|
||||
event: &galaxyui::event::DispatchedEvent,
|
||||
ctx: &mut galaxyui::EventContext,
|
||||
event: &galaxyui_core::event::DispatchedEvent,
|
||||
ctx: &mut galaxyui_core::EventContext,
|
||||
app: &AppContext,
|
||||
) -> bool {
|
||||
self.element.dispatch_event(event, ctx, app)
|
||||
|
||||
@@ -1,20 +1,12 @@
|
||||
use galaxyui::{
|
||||
elements::{CornerRadius, Radius},
|
||||
geometry::{
|
||||
rect::RectF,
|
||||
vector::{Vector2F, vec2f},
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
extract_block,
|
||||
render::{
|
||||
element::paint::{CursorData, CursorDisplayType},
|
||||
model::{BlockItem, RenderState, RichTextStyles, viewport::ViewportItem},
|
||||
},
|
||||
};
|
||||
use galaxyui_core::elements::{CornerRadius, Radius};
|
||||
use galaxyui_core::geometry::rect::RectF;
|
||||
use galaxyui_core::geometry::vector::{Vector2F, vec2f};
|
||||
|
||||
use super::{RenderContext, RenderableBlock};
|
||||
use crate::extract_block;
|
||||
use crate::render::element::paint::{CursorData, CursorDisplayType};
|
||||
use crate::render::model::viewport::ViewportItem;
|
||||
use crate::render::model::{BlockItem, RenderState, RichTextStyles};
|
||||
|
||||
/// Renderable representation of a single horizontal rule separator.
|
||||
pub struct HorizontalRule {
|
||||
@@ -77,12 +69,17 @@ impl RenderableBlock for HorizontalRule {
|
||||
fn layout(
|
||||
&mut self,
|
||||
_model: &RenderState,
|
||||
_ctx: &mut galaxyui::LayoutContext,
|
||||
_app: &galaxyui::AppContext,
|
||||
_ctx: &mut galaxyui_core::LayoutContext,
|
||||
_app: &galaxyui_core::AppContext,
|
||||
) {
|
||||
}
|
||||
|
||||
fn paint(&mut self, model: &RenderState, ctx: &mut RenderContext, _app: &galaxyui::AppContext) {
|
||||
fn paint(
|
||||
&mut self,
|
||||
model: &RenderState,
|
||||
ctx: &mut RenderContext,
|
||||
_app: &galaxyui_core::AppContext,
|
||||
) {
|
||||
let content = model.content();
|
||||
let horizontal_rule = extract_block!(self.viewport_item, content, (block, BlockItem::HorizontalRule(rule)) => block.horizontal_rule(rule));
|
||||
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
use galaxyui::{
|
||||
Element, SizeConstraint,
|
||||
elements::{CacheOption, Image},
|
||||
geometry::vector::vec2f,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
extract_block,
|
||||
render::{
|
||||
element::paint::{CursorData, CursorDisplayType},
|
||||
model::{BlockItem, RenderState, viewport::ViewportItem},
|
||||
},
|
||||
};
|
||||
use galaxyui_core::elements::{CacheOption, Image};
|
||||
use galaxyui_core::geometry::vector::vec2f;
|
||||
use galaxyui_core::{Element, SizeConstraint};
|
||||
|
||||
use super::{RenderContext, RenderableBlock};
|
||||
use crate::extract_block;
|
||||
use crate::render::element::paint::{CursorData, CursorDisplayType};
|
||||
use crate::render::model::viewport::ViewportItem;
|
||||
use crate::render::model::{BlockItem, RenderState};
|
||||
|
||||
pub struct RenderableImage {
|
||||
viewport_item: ViewportItem,
|
||||
@@ -39,8 +33,8 @@ impl RenderableBlock for RenderableImage {
|
||||
fn layout(
|
||||
&mut self,
|
||||
model: &RenderState,
|
||||
ctx: &mut galaxyui::LayoutContext,
|
||||
app: &galaxyui::AppContext,
|
||||
ctx: &mut galaxyui_core::LayoutContext,
|
||||
app: &galaxyui_core::AppContext,
|
||||
) {
|
||||
let content = model.content();
|
||||
let (asset_source, config) = extract_block!(
|
||||
@@ -60,7 +54,12 @@ impl RenderableBlock for RenderableImage {
|
||||
self.image_element = Some(Box::new(image));
|
||||
}
|
||||
|
||||
fn paint(&mut self, model: &RenderState, ctx: &mut RenderContext, app: &galaxyui::AppContext) {
|
||||
fn paint(
|
||||
&mut self,
|
||||
model: &RenderState,
|
||||
ctx: &mut RenderContext,
|
||||
app: &galaxyui_core::AppContext,
|
||||
) {
|
||||
let content = model.content();
|
||||
let positioned_image = extract_block!(
|
||||
self.viewport_item,
|
||||
@@ -83,7 +82,7 @@ impl RenderableBlock for RenderableImage {
|
||||
}
|
||||
|
||||
if selected {
|
||||
let rect_bounds = galaxyui::geometry::rect::RectF::new(screen_position, size);
|
||||
let rect_bounds = galaxyui_core::geometry::rect::RectF::new(screen_position, size);
|
||||
ctx.paint
|
||||
.scene
|
||||
.draw_rect_with_hit_recording(rect_bounds)
|
||||
|
||||
@@ -1,24 +1,20 @@
|
||||
use std::ops::Range;
|
||||
|
||||
use galaxyui::{
|
||||
use galaxyui_core::elements::Point;
|
||||
use galaxyui_core::event::DispatchedEvent;
|
||||
use galaxyui_core::geometry::rect::RectF;
|
||||
use galaxyui_core::geometry::vector::Vector2F;
|
||||
use galaxyui_core::units::IntoPixels;
|
||||
use galaxyui_core::{
|
||||
AfterLayoutContext, AppContext, Element, EventContext, LayoutContext, ModelHandle,
|
||||
PaintContext, SizeConstraint, WeakViewHandle,
|
||||
elements::Point,
|
||||
event::DispatchedEvent,
|
||||
geometry::{rect::RectF, vector::Vector2F},
|
||||
units::IntoPixels,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
editor::EditorView,
|
||||
render::{
|
||||
element::{
|
||||
DisplayOptions, RenderContext, RenderableBlock, paragraph::RenderableParagraph,
|
||||
temporary_block::RenderableTemporaryBlock,
|
||||
},
|
||||
model::{BlockItem, RenderLineLocation, RenderState},
|
||||
},
|
||||
};
|
||||
use crate::editor::EditorView;
|
||||
use crate::render::element::paragraph::RenderableParagraph;
|
||||
use crate::render::element::temporary_block::RenderableTemporaryBlock;
|
||||
use crate::render::element::{DisplayOptions, RenderContext, RenderableBlock};
|
||||
use crate::render::model::{BlockItem, RenderLineLocation, RenderState};
|
||||
|
||||
pub struct RichTextElementLens<V: EditorView> {
|
||||
blocks: Option<Vec<Box<dyn RenderableBlock>>>,
|
||||
|
||||
@@ -1,29 +1,39 @@
|
||||
use galaxyui::{
|
||||
AppContext, Element, SizeConstraint,
|
||||
elements::{Align, CacheOption, CornerRadius, Image, Radius, Text},
|
||||
geometry::vector::vec2f,
|
||||
};
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::{
|
||||
extract_block,
|
||||
render::{
|
||||
element::paint::CursorData,
|
||||
model::{BlockItem, RenderState, viewport::ViewportItem},
|
||||
},
|
||||
};
|
||||
use galaxyui_core::elements::{Align, CacheOption, CornerRadius, Empty, Image, Radius, Text};
|
||||
use galaxyui_core::geometry::vector::vec2f;
|
||||
use galaxyui_core::{AppContext, Element, SizeConstraint};
|
||||
|
||||
use super::{CursorDisplayType, RenderContext, RenderableBlock};
|
||||
use super::{RenderContext, RenderableBlock};
|
||||
use crate::editor::RunnableCommandModel;
|
||||
use crate::extract_block;
|
||||
use crate::render::BLOCK_FOOTER_HEIGHT;
|
||||
use crate::render::element::paint::{CursorData, CursorDisplayType};
|
||||
use crate::render::model::viewport::ViewportItem;
|
||||
use crate::render::model::{BlockItem, RenderState, bounds};
|
||||
const MERMAID_RENDER_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
pub struct RenderableMermaidDiagram {
|
||||
viewport_item: ViewportItem,
|
||||
image_element: Option<Box<dyn Element>>,
|
||||
footer: Box<dyn Element>,
|
||||
}
|
||||
|
||||
impl RenderableMermaidDiagram {
|
||||
pub fn new(viewport_item: ViewportItem) -> Self {
|
||||
pub fn new(
|
||||
viewport_item: ViewportItem,
|
||||
model: Option<&dyn RunnableCommandModel>,
|
||||
editor_is_focused: bool,
|
||||
ctx: &AppContext,
|
||||
) -> Self {
|
||||
let footer = match model {
|
||||
Some(model) => model.render_block_footer(editor_is_focused, ctx),
|
||||
None => Empty::new().finish(),
|
||||
};
|
||||
Self {
|
||||
viewport_item,
|
||||
image_element: None,
|
||||
footer,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,7 +43,12 @@ impl RenderableBlock for RenderableMermaidDiagram {
|
||||
&self.viewport_item
|
||||
}
|
||||
|
||||
fn layout(&mut self, model: &RenderState, ctx: &mut galaxyui::LayoutContext, app: &AppContext) {
|
||||
fn layout(
|
||||
&mut self,
|
||||
model: &RenderState,
|
||||
ctx: &mut galaxyui_core::LayoutContext,
|
||||
app: &AppContext,
|
||||
) {
|
||||
let content = model.content();
|
||||
let (asset_source, config) = extract_block!(
|
||||
self.viewport_item,
|
||||
@@ -41,14 +56,48 @@ impl RenderableBlock for RenderableMermaidDiagram {
|
||||
(_block, BlockItem::MermaidDiagram { asset_source, config, .. }) => (asset_source.clone(), *config)
|
||||
);
|
||||
|
||||
self.footer.layout(
|
||||
SizeConstraint::strict(vec2f(
|
||||
self.viewport_item.content_size.x(),
|
||||
BLOCK_FOOTER_HEIGHT,
|
||||
)),
|
||||
ctx,
|
||||
app,
|
||||
);
|
||||
|
||||
let code_text = model.styles().code_text;
|
||||
let placeholder_color = model.styles().placeholder_color;
|
||||
let placeholder = Align::new(
|
||||
Text::new(
|
||||
"Rendering Mermaid diagram…",
|
||||
code_text.font_family,
|
||||
code_text.font_size,
|
||||
)
|
||||
.with_color(model.styles().placeholder_color)
|
||||
.with_color(placeholder_color)
|
||||
.with_line_height_ratio(code_text.line_height_ratio)
|
||||
.soft_wrap(false)
|
||||
.finish(),
|
||||
)
|
||||
.finish();
|
||||
let failure_notice = Align::new(
|
||||
Text::new(
|
||||
"Error rendering Mermaid diagram. Please check syntax.",
|
||||
code_text.font_family,
|
||||
code_text.font_size,
|
||||
)
|
||||
.with_color(placeholder_color)
|
||||
.with_line_height_ratio(code_text.line_height_ratio)
|
||||
.soft_wrap(true)
|
||||
.finish(),
|
||||
)
|
||||
.finish();
|
||||
let timeout_notice = Align::new(
|
||||
Text::new(
|
||||
"Failed to render Mermaid diagram",
|
||||
code_text.font_family,
|
||||
code_text.font_size,
|
||||
)
|
||||
.with_color(placeholder_color)
|
||||
.with_line_height_ratio(code_text.line_height_ratio)
|
||||
.soft_wrap(false)
|
||||
.finish(),
|
||||
@@ -58,7 +107,9 @@ impl RenderableBlock for RenderableMermaidDiagram {
|
||||
let size = vec2f(config.width.as_f32(), config.height.as_f32());
|
||||
let mut image = Image::new(asset_source, CacheOption::BySize)
|
||||
.contain()
|
||||
.before_load(placeholder);
|
||||
.before_load(placeholder)
|
||||
.on_load_failure(failure_notice)
|
||||
.on_load_timeout(MERMAID_RENDER_TIMEOUT, timeout_notice);
|
||||
image.layout(SizeConstraint::strict(size), ctx, app);
|
||||
|
||||
self.image_element = Some(Box::new(image));
|
||||
@@ -96,11 +147,16 @@ impl RenderableBlock for RenderableMermaidDiagram {
|
||||
.draw_rect_with_hit_recording(content_rect)
|
||||
.with_background(model.styles().selection_fill);
|
||||
}
|
||||
|
||||
if model.is_selection_head(start_offset) {
|
||||
let content_position = bounds::content_origin(
|
||||
self.viewport_item.content_offset,
|
||||
&self.viewport_item.spacing,
|
||||
);
|
||||
let end_of_line_position =
|
||||
content_position + vec2f(self.viewport_item.content_size.x(), 0.);
|
||||
ctx.draw_and_save_cursor(
|
||||
CursorDisplayType::Bar,
|
||||
content_rect.origin(),
|
||||
end_of_line_position,
|
||||
vec2f(
|
||||
model.styles().cursor_width,
|
||||
self.viewport_item.content_size.y(),
|
||||
@@ -109,11 +165,36 @@ impl RenderableBlock for RenderableMermaidDiagram {
|
||||
model.styles(),
|
||||
);
|
||||
}
|
||||
ctx.paint
|
||||
.scene
|
||||
.start_layer(galaxyui_core::ClipBounds::ActiveLayer);
|
||||
let button_origin = content_rect.lower_right()
|
||||
- vec2f(
|
||||
self.footer.size().expect("Footer should be laid out").x(),
|
||||
0.,
|
||||
);
|
||||
self.footer.paint(button_origin, ctx.paint, app);
|
||||
ctx.paint.scene.stop_layer();
|
||||
}
|
||||
|
||||
fn after_layout(&mut self, ctx: &mut galaxyui::AfterLayoutContext, app: &galaxyui::AppContext) {
|
||||
fn after_layout(
|
||||
&mut self,
|
||||
ctx: &mut galaxyui_core::AfterLayoutContext,
|
||||
app: &galaxyui_core::AppContext,
|
||||
) {
|
||||
if let Some(ref mut image_element) = self.image_element {
|
||||
image_element.after_layout(ctx, app);
|
||||
}
|
||||
self.footer.after_layout(ctx, app);
|
||||
}
|
||||
|
||||
fn dispatch_event(
|
||||
&mut self,
|
||||
_model: &RenderState,
|
||||
event: &galaxyui_core::event::DispatchedEvent,
|
||||
ctx: &mut galaxyui_core::EventContext,
|
||||
app: &AppContext,
|
||||
) -> bool {
|
||||
self.footer.dispatch_event(event, ctx, app)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,29 @@
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::Duration;
|
||||
|
||||
use float_cmp::ApproxEq;
|
||||
use galaxy_core::ui::theme::Fill as ThemeFill;
|
||||
use galaxyui::{
|
||||
AfterLayoutContext, AppContext, Element, Event, EventContext, LayoutContext, ModelHandle,
|
||||
PaintContext, SizeConstraint, WeakViewHandle,
|
||||
color::ColorU,
|
||||
elements::{
|
||||
Axis, Border, Dash, Point, ScrollData, ScrollableElement, Vector2FExt, ZIndex,
|
||||
new_scrollable::{NewScrollableElement, ScrollableAxis},
|
||||
},
|
||||
event::{DispatchedEvent, ModifiersState},
|
||||
geometry::{
|
||||
rect::RectF,
|
||||
vector::{Vector2F, vec2f},
|
||||
},
|
||||
platform::Cursor,
|
||||
units::{IntoPixels, Pixels},
|
||||
};
|
||||
use instant::Instant;
|
||||
use parking_lot::Mutex;
|
||||
use string_offset::CharOffset;
|
||||
use temporary_block::RenderableTemporaryBlock;
|
||||
use vim::vim::VimMode;
|
||||
use galaxy_core::ui::theme::Fill as ThemeFill;
|
||||
use galaxyui_core::color::ColorU;
|
||||
use galaxyui_core::elements::new_scrollable::{NewScrollableElement, ScrollableAxis};
|
||||
use galaxyui_core::elements::{
|
||||
Axis, Border, Dash, Point, ScrollData, ScrollableElement, Vector2FExt, ZIndex,
|
||||
};
|
||||
use galaxyui_core::event::{DispatchedEvent, ModifiersState};
|
||||
use galaxyui_core::geometry::rect::RectF;
|
||||
use galaxyui_core::geometry::vector::{Vector2F, vec2f};
|
||||
use galaxyui_core::platform::Cursor;
|
||||
use galaxyui_core::units::{IntoPixels, Pixels};
|
||||
use galaxyui_core::{
|
||||
AfterLayoutContext, AppContext, Element, Event, EventContext, LayoutContext, ModelHandle,
|
||||
PaintContext, SizeConstraint, WeakViewHandle,
|
||||
};
|
||||
use std::{
|
||||
fmt,
|
||||
sync::{
|
||||
@@ -26,26 +32,27 @@ use std::{
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
use temporary_block::RenderableTemporaryBlock;
|
||||
use vim::vim::VimMode;
|
||||
|
||||
use self::empty::Empty;
|
||||
use self::header::RenderableHeader;
|
||||
use self::hidden_section::RenderableHiddenSection;
|
||||
use self::horizontal_rule::HorizontalRule;
|
||||
use self::image::RenderableImage;
|
||||
use self::mermaid::RenderableMermaidDiagram;
|
||||
use self::ordered_list::RenderableOrderedListItem;
|
||||
pub use self::paint::{CursorData, CursorDisplayType, RenderContext};
|
||||
use self::paragraph::RenderableParagraph;
|
||||
use self::runnable_command::RenderableRunnableCommand;
|
||||
use self::table::RenderableTable;
|
||||
use self::task_list::RenderableTaskList;
|
||||
use self::text_block::RenderableTextBlock;
|
||||
use self::unordered_list::RenderableBulletList;
|
||||
use super::model::viewport::{SizeInfo, ViewportItem};
|
||||
use super::model::{
|
||||
BlockItem, ElementUpdate, HitTestOptions, Location, RenderState, RichTextStyles, UNIT_MARGIN,
|
||||
viewport::{SizeInfo, ViewportItem},
|
||||
};
|
||||
use crate::{content::version::BufferVersion, editor::EditorView};
|
||||
use string_offset::CharOffset;
|
||||
|
||||
use self::{
|
||||
empty::Empty, header::RenderableHeader, hidden_section::RenderableHiddenSection,
|
||||
horizontal_rule::HorizontalRule, image::RenderableImage, mermaid::RenderableMermaidDiagram,
|
||||
ordered_list::RenderableOrderedListItem, paragraph::RenderableParagraph,
|
||||
runnable_command::RenderableRunnableCommand, table::RenderableTable,
|
||||
task_list::RenderableTaskList, text_block::RenderableTextBlock,
|
||||
unordered_list::RenderableBulletList,
|
||||
};
|
||||
|
||||
pub use self::paint::{CursorData, CursorDisplayType, RenderContext};
|
||||
use crate::content::version::BufferVersion;
|
||||
use crate::editor::EditorView;
|
||||
|
||||
pub mod broken_embedding;
|
||||
mod empty;
|
||||
@@ -85,7 +92,7 @@ pub enum VerticalExpansionBehavior {
|
||||
/// An element that renders rich text, with no additional UI or decorations.
|
||||
///
|
||||
/// This element caches the positions listed in [`super::model::saved_positions::SavedPositions`],
|
||||
/// and the parent view can overlay UI controls on top of them using a [`galaxyui::elements::Stack`].
|
||||
/// and the parent view can overlay UI controls on top of them using a [`galaxyui_core::elements::Stack`].
|
||||
///
|
||||
/// It additionally reserves horizontal gutters, which are considered in-bounds for content hit
|
||||
/// testing.
|
||||
@@ -886,7 +893,15 @@ impl<V: EditorView> RichTextElement<V> {
|
||||
.finish()
|
||||
}
|
||||
BlockItem::MermaidDiagram { .. } => {
|
||||
RenderableMermaidDiagram::new(item).finish()
|
||||
let start_offset = item.block_offset;
|
||||
let runnable_command = parent.runnable_command_at(start_offset, ctx);
|
||||
RenderableMermaidDiagram::new(
|
||||
item,
|
||||
runnable_command,
|
||||
self.display_options.focused,
|
||||
ctx,
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
BlockItem::TemporaryBlock {
|
||||
decoration,
|
||||
@@ -1077,7 +1092,7 @@ impl<V: EditorView> Element for RichTextElement<V> {
|
||||
return;
|
||||
};
|
||||
ctx.scene
|
||||
.start_layer(galaxyui::ClipBounds::BoundedBy(clip_bounds));
|
||||
.start_layer(galaxyui_core::ClipBounds::BoundedBy(clip_bounds));
|
||||
// Save the clipped content layer z-index for hover detection.
|
||||
self.content_z_index = Some(ctx.scene.z_index());
|
||||
|
||||
@@ -1236,7 +1251,7 @@ impl<V: EditorView> NewScrollableElement for RichTextElement<V> {
|
||||
})
|
||||
}
|
||||
|
||||
fn scroll(&mut self, delta: galaxyui::units::Pixels, axis: Axis, ctx: &mut EventContext) {
|
||||
fn scroll(&mut self, delta: galaxyui_core::units::Pixels, axis: Axis, ctx: &mut EventContext) {
|
||||
if let Some(action) = V::Action::scroll(delta, axis) {
|
||||
ctx.dispatch_typed_action(action);
|
||||
}
|
||||
@@ -1257,7 +1272,7 @@ impl<V: EditorView> ScrollableElement for RichTextElement<V> {
|
||||
Some(self.vertical_scroll_data(app))
|
||||
}
|
||||
|
||||
fn scroll(&mut self, delta: galaxyui::units::Pixels, ctx: &mut EventContext) {
|
||||
fn scroll(&mut self, delta: galaxyui_core::units::Pixels, ctx: &mut EventContext) {
|
||||
if let Some(action) = V::Action::scroll(delta, Axis::Vertical) {
|
||||
ctx.dispatch_typed_action(action);
|
||||
}
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
content::text::BufferBlockStyle,
|
||||
extract_block,
|
||||
render::{
|
||||
layout::TextLayout,
|
||||
model::{BlockItem, RenderState, viewport::ViewportItem},
|
||||
},
|
||||
};
|
||||
use galaxyui::elements::ListIndentLevel;
|
||||
use galaxyui::{geometry::vector::vec2f, text_layout::TextFrame};
|
||||
use galaxyui_core::elements::ListIndentLevel;
|
||||
use galaxyui_core::geometry::vector::vec2f;
|
||||
use galaxyui_core::text_layout::TextFrame;
|
||||
|
||||
use super::{
|
||||
RenderableBlock,
|
||||
paint::RenderContext,
|
||||
placeholder::{self, BlockPlaceholder},
|
||||
};
|
||||
use super::RenderableBlock;
|
||||
use super::paint::RenderContext;
|
||||
use super::placeholder::{self, BlockPlaceholder};
|
||||
use crate::content::text::BufferBlockStyle;
|
||||
use crate::extract_block;
|
||||
use crate::render::layout::TextLayout;
|
||||
use crate::render::model::viewport::ViewportItem;
|
||||
use crate::render::model::{BlockItem, RenderState};
|
||||
|
||||
pub struct RenderableOrderedListItem {
|
||||
viewport_item: ViewportItem,
|
||||
@@ -44,8 +40,8 @@ impl RenderableBlock for RenderableOrderedListItem {
|
||||
fn layout(
|
||||
&mut self,
|
||||
model: &RenderState,
|
||||
ctx: &mut galaxyui::LayoutContext,
|
||||
app: &galaxyui::AppContext,
|
||||
ctx: &mut galaxyui_core::LayoutContext,
|
||||
app: &galaxyui_core::AppContext,
|
||||
) {
|
||||
let text_layout = TextLayout::from_layout_context(ctx, app, model);
|
||||
let block_style = BufferBlockStyle::OrderedList {
|
||||
@@ -75,7 +71,12 @@ impl RenderableBlock for RenderableOrderedListItem {
|
||||
});
|
||||
}
|
||||
|
||||
fn paint(&mut self, model: &RenderState, ctx: &mut RenderContext, _app: &galaxyui::AppContext) {
|
||||
fn paint(
|
||||
&mut self,
|
||||
model: &RenderState,
|
||||
ctx: &mut RenderContext,
|
||||
_app: &galaxyui_core::AppContext,
|
||||
) {
|
||||
let content = model.content();
|
||||
let paragraph = extract_block!(self.viewport_item, content, (block, BlockItem::OrderedList{ paragraph: inner, ..}) => block.ordered_list(inner));
|
||||
|
||||
|
||||
@@ -2,29 +2,21 @@
|
||||
|
||||
use std::ops::Range;
|
||||
|
||||
use galaxy_core::ui::appearance::DEFAULT_UI_FONT_SIZE;
|
||||
use galaxyui::{
|
||||
PaintContext,
|
||||
elements::{CornerRadius, Point, Radius},
|
||||
geometry::{
|
||||
rect::RectF,
|
||||
vector::{Vector2F, vec2f},
|
||||
},
|
||||
text_layout::{Line, PaintStyleOverride, TextFrame},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
editor::TextDecoration,
|
||||
render::{
|
||||
layout::line_height,
|
||||
model::{
|
||||
Decoration, Paragraph, ParagraphStyles, Positioned, RenderState, RichTextStyles,
|
||||
saved_positions::SavedPositions,
|
||||
},
|
||||
},
|
||||
};
|
||||
use string_offset::CharOffset;
|
||||
use vim::vim::VimMode;
|
||||
use galaxy_core::ui::appearance::DEFAULT_UI_FONT_SIZE;
|
||||
use galaxyui_core::PaintContext;
|
||||
use galaxyui_core::elements::{CornerRadius, Point, Radius};
|
||||
use galaxyui_core::geometry::rect::RectF;
|
||||
use galaxyui_core::geometry::vector::{Vector2F, vec2f};
|
||||
use galaxyui_core::text_layout::{Line, PaintStyleOverride, TextFrame};
|
||||
|
||||
use crate::editor::TextDecoration;
|
||||
use crate::render::layout::line_height;
|
||||
use crate::render::model::saved_positions::SavedPositions;
|
||||
use crate::render::model::{
|
||||
Decoration, Paragraph, ParagraphStyles, Positioned, RenderState, RichTextStyles,
|
||||
};
|
||||
|
||||
const DEFAULT_BLOCK_CURSOR_WIDTH: f32 = 8.;
|
||||
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
use crate::{
|
||||
content::text::BufferBlockStyle,
|
||||
extract_block,
|
||||
render::model::{BlockItem, RenderState, viewport::ViewportItem},
|
||||
};
|
||||
|
||||
use super::{
|
||||
RenderableBlock,
|
||||
paint::RenderContext,
|
||||
placeholder::{self, BlockPlaceholder},
|
||||
};
|
||||
use super::RenderableBlock;
|
||||
use super::paint::RenderContext;
|
||||
use super::placeholder::{self, BlockPlaceholder};
|
||||
use crate::content::text::BufferBlockStyle;
|
||||
use crate::extract_block;
|
||||
use crate::render::model::viewport::ViewportItem;
|
||||
use crate::render::model::{BlockItem, RenderState};
|
||||
|
||||
/// The placeholder text to show in empty plain-text blocks.
|
||||
pub(super) const PARAGRAPH_PLACEHOLDER_TEXT: &str =
|
||||
@@ -47,8 +43,8 @@ impl RenderableBlock for RenderableParagraph {
|
||||
fn layout(
|
||||
&mut self,
|
||||
model: &RenderState,
|
||||
ctx: &mut galaxyui::LayoutContext,
|
||||
app: &galaxyui::AppContext,
|
||||
ctx: &mut galaxyui_core::LayoutContext,
|
||||
app: &galaxyui_core::AppContext,
|
||||
) {
|
||||
self.placeholder
|
||||
.layout(&self.viewport_item, model, ctx, app, |_| {
|
||||
@@ -59,7 +55,12 @@ impl RenderableBlock for RenderableParagraph {
|
||||
});
|
||||
}
|
||||
|
||||
fn paint(&mut self, model: &RenderState, ctx: &mut RenderContext, _app: &galaxyui::AppContext) {
|
||||
fn paint(
|
||||
&mut self,
|
||||
model: &RenderState,
|
||||
ctx: &mut RenderContext,
|
||||
_app: &galaxyui_core::AppContext,
|
||||
) {
|
||||
let content = model.content();
|
||||
let paragraph = extract_block!(self.viewport_item, content, (block, BlockItem::Paragraph(inner)) => block.paragraph(inner));
|
||||
|
||||
|
||||
@@ -1,21 +1,15 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use galaxyui::{
|
||||
AppContext, LayoutContext,
|
||||
geometry::vector::{Vector2F, vec2f},
|
||||
text_layout::Line,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
content::text::BufferBlockStyle,
|
||||
render::{
|
||||
element::paint::CursorDisplayType,
|
||||
layout::{TextLayout, line_height},
|
||||
model::{BlockItem, RenderState, viewport::ViewportItem},
|
||||
},
|
||||
};
|
||||
use galaxyui_core::geometry::vector::{Vector2F, vec2f};
|
||||
use galaxyui_core::text_layout::Line;
|
||||
use galaxyui_core::{AppContext, LayoutContext};
|
||||
|
||||
use super::{CursorData, RenderContext};
|
||||
use crate::content::text::BufferBlockStyle;
|
||||
use crate::render::element::paint::CursorDisplayType;
|
||||
use crate::render::layout::{TextLayout, line_height};
|
||||
use crate::render::model::viewport::ViewportItem;
|
||||
use crate::render::model::{BlockItem, RenderState};
|
||||
|
||||
/// Ghost/placeholder text that's shown in an empty block to provide context.
|
||||
pub struct BlockPlaceholder {
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
use galaxyui::{
|
||||
AppContext, Element, SizeConstraint,
|
||||
elements::{Border, CornerRadius, Empty, Radius},
|
||||
geometry::vector::vec2f,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
editor::RunnableCommandModel,
|
||||
extract_block,
|
||||
render::{
|
||||
BLOCK_FOOTER_HEIGHT,
|
||||
model::{BlockItem, RenderState, viewport::ViewportItem},
|
||||
},
|
||||
};
|
||||
use galaxyui_core::elements::{Border, CornerRadius, Empty, Radius};
|
||||
use galaxyui_core::geometry::vector::vec2f;
|
||||
use galaxyui_core::{AppContext, Element, SizeConstraint};
|
||||
|
||||
use super::{RenderContext, RenderableBlock};
|
||||
use crate::editor::RunnableCommandModel;
|
||||
use crate::extract_block;
|
||||
use crate::render::BLOCK_FOOTER_HEIGHT;
|
||||
use crate::render::model::viewport::ViewportItem;
|
||||
use crate::render::model::{BlockItem, RenderState};
|
||||
|
||||
/// [`RenderableBlock`] implementation for runnable command blocks.
|
||||
pub struct RenderableRunnableCommand {
|
||||
@@ -51,7 +45,7 @@ impl RenderableBlock for RenderableRunnableCommand {
|
||||
fn layout(
|
||||
&mut self,
|
||||
_model: &RenderState,
|
||||
ctx: &mut galaxyui::LayoutContext,
|
||||
ctx: &mut galaxyui_core::LayoutContext,
|
||||
app: &AppContext,
|
||||
) {
|
||||
self.footer.layout(
|
||||
@@ -66,7 +60,7 @@ impl RenderableBlock for RenderableRunnableCommand {
|
||||
|
||||
fn paint(&mut self, model: &RenderState, ctx: &mut RenderContext, app: &AppContext) {
|
||||
let content = model.content();
|
||||
let code_block = extract_block!(self.viewport_item, content, (block, BlockItem::RunnableCodeBlock{code_block_type: _, paragraph_block}) => block.code_block(paragraph_block));
|
||||
let code_block = extract_block!(self.viewport_item, content, (block, BlockItem::RunnableCodeBlock{paragraph_block, ..}) => block.code_block(paragraph_block));
|
||||
|
||||
let styles = model.styles();
|
||||
let code_style = &styles.code_text;
|
||||
@@ -94,7 +88,7 @@ impl RenderableBlock for RenderableRunnableCommand {
|
||||
// `RichTextElement::content_z_index` for context.
|
||||
ctx.paint
|
||||
.scene
|
||||
.start_layer(galaxyui::ClipBounds::ActiveLayer);
|
||||
.start_layer(galaxyui_core::ClipBounds::ActiveLayer);
|
||||
|
||||
// Position the block footer right below the content area, flush with its right-hand edge.
|
||||
// This gives the footer some padding relative to the visible area with a background.
|
||||
@@ -109,15 +103,19 @@ impl RenderableBlock for RenderableRunnableCommand {
|
||||
ctx.paint.scene.stop_layer();
|
||||
}
|
||||
|
||||
fn after_layout(&mut self, ctx: &mut galaxyui::AfterLayoutContext, app: &galaxyui::AppContext) {
|
||||
fn after_layout(
|
||||
&mut self,
|
||||
ctx: &mut galaxyui_core::AfterLayoutContext,
|
||||
app: &galaxyui_core::AppContext,
|
||||
) {
|
||||
self.footer.after_layout(ctx, app);
|
||||
}
|
||||
|
||||
fn dispatch_event(
|
||||
&mut self,
|
||||
_model: &crate::render::model::RenderState,
|
||||
event: &galaxyui::event::DispatchedEvent,
|
||||
ctx: &mut galaxyui::EventContext,
|
||||
event: &galaxyui_core::event::DispatchedEvent,
|
||||
ctx: &mut galaxyui_core::EventContext,
|
||||
app: &AppContext,
|
||||
) -> bool {
|
||||
self.footer.dispatch_event(event, ctx, app)
|
||||
|
||||
@@ -1,32 +1,23 @@
|
||||
use galaxyui::{
|
||||
AppContext, ClipBounds, Event, EventContext,
|
||||
elements::{
|
||||
Axis, CornerRadius, DEFAULT_SCROLL_WHEEL_PIXELS_PER_LINE, Radius, ScrollData,
|
||||
ScrollbarAppearance, ScrollbarGeometry, ScrollbarWidth, compute_scrollbar_geometry,
|
||||
project_scroll_delta_by_sensitivity, scroll_delta_for_pointer_movement,
|
||||
},
|
||||
event::DispatchedEvent,
|
||||
geometry::{
|
||||
rect::RectF,
|
||||
vector::{Vector2F, vec2f},
|
||||
},
|
||||
units::{IntoPixels, Pixels},
|
||||
};
|
||||
use std::ops::Range;
|
||||
|
||||
use string_offset::CharOffset;
|
||||
use galaxyui_core::elements::{
|
||||
Axis, CornerRadius, DEFAULT_SCROLL_WHEEL_PIXELS_PER_LINE, Radius, ScrollData,
|
||||
ScrollbarAppearance, ScrollbarGeometry, ScrollbarWidth, compute_scrollbar_geometry,
|
||||
project_scroll_delta_by_sensitivity, scroll_delta_for_pointer_movement,
|
||||
};
|
||||
use galaxyui_core::event::DispatchedEvent;
|
||||
use galaxyui_core::geometry::rect::RectF;
|
||||
use galaxyui_core::geometry::vector::{Vector2F, vec2f};
|
||||
use galaxyui_core::units::{IntoPixels, Pixels};
|
||||
use galaxyui_core::{AppContext, ClipBounds, Event, EventContext};
|
||||
|
||||
use super::paint::{CursorData, CursorDisplayType};
|
||||
use super::{RenderContext, RenderableBlock};
|
||||
use crate::extract_block;
|
||||
use crate::render::model::table_offset_map::CellAtOffset;
|
||||
use crate::{
|
||||
extract_block,
|
||||
render::model::{
|
||||
BlockItem, LaidOutTable, RenderState, RenderedSelection, TableStyle, viewport::ViewportItem,
|
||||
},
|
||||
};
|
||||
|
||||
use super::{
|
||||
RenderContext, RenderableBlock,
|
||||
paint::{CursorData, CursorDisplayType},
|
||||
};
|
||||
use crate::render::model::viewport::ViewportItem;
|
||||
use crate::render::model::{BlockItem, LaidOutTable, RenderState, RenderedSelection, TableStyle};
|
||||
|
||||
const TABLE_BORDER_WIDTH: f32 = 1.0;
|
||||
const TABLE_SCROLL_SENSITIVITY: f32 = 1.0;
|
||||
@@ -121,7 +112,7 @@ impl RenderableBlock for RenderableTable {
|
||||
&self.viewport_item
|
||||
}
|
||||
|
||||
fn layout(&mut self, _: &RenderState, _: &mut galaxyui::LayoutContext, _: &AppContext) {}
|
||||
fn layout(&mut self, _: &RenderState, _: &mut galaxyui_core::LayoutContext, _: &AppContext) {}
|
||||
|
||||
fn paint(&mut self, model: &RenderState, ctx: &mut RenderContext, _app: &AppContext) {
|
||||
let content = model.content();
|
||||
|
||||
@@ -1,27 +1,24 @@
|
||||
use galaxyui::{
|
||||
elements::{Axis, scroll_delta_for_pointer_movement},
|
||||
fonts::FamilyId,
|
||||
geometry::{rect::RectF, vector::vec2f},
|
||||
text_layout::TextFrame,
|
||||
units::{IntoPixels, Pixels},
|
||||
};
|
||||
use pathfinder_color::ColorU;
|
||||
use std::{cell::Cell, sync::Arc};
|
||||
use string_offset::CharOffset;
|
||||
use std::cell::Cell;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
content::text::{FormattedTable, table_cell_offset_maps},
|
||||
render::{
|
||||
element::table::{
|
||||
model_table_layout_report, row_geometry_from_layout_report,
|
||||
table_cursor_relative_offset, table_horizontal_scroll_delta, table_scroll_data,
|
||||
table_scrollbar, table_selection_relative_range,
|
||||
},
|
||||
model::{
|
||||
BlockSpacing, CellLayout, LaidOutTable, RenderedSelection, TableBlockConfig,
|
||||
TableStyle, table_offset_map::TableOffsetMap,
|
||||
},
|
||||
},
|
||||
use pathfinder_color::ColorU;
|
||||
use string_offset::CharOffset;
|
||||
use galaxyui_core::elements::{Axis, scroll_delta_for_pointer_movement};
|
||||
use galaxyui_core::fonts::FamilyId;
|
||||
use galaxyui_core::geometry::rect::RectF;
|
||||
use galaxyui_core::geometry::vector::vec2f;
|
||||
use galaxyui_core::text_layout::TextFrame;
|
||||
use galaxyui_core::units::{IntoPixels, Pixels};
|
||||
|
||||
use crate::content::text::{FormattedTable, table_cell_offset_maps};
|
||||
use crate::render::element::table::{
|
||||
model_table_layout_report, row_geometry_from_layout_report, table_cursor_relative_offset,
|
||||
table_horizontal_scroll_delta, table_scroll_data, table_scrollbar,
|
||||
table_selection_relative_range,
|
||||
};
|
||||
use crate::render::model::table_offset_map::TableOffsetMap;
|
||||
use crate::render::model::{
|
||||
BlockSpacing, CellLayout, LaidOutTable, RenderedSelection, TableBlockConfig, TableStyle,
|
||||
};
|
||||
|
||||
fn test_laid_out_table() -> LaidOutTable {
|
||||
@@ -214,7 +211,7 @@ fn cells_in_range_entire_table() {
|
||||
}
|
||||
|
||||
fn single_line_cell_layout(char_count: usize, line_height: f32, line_width: f32) -> CellLayout {
|
||||
use galaxyui::text_layout::CaretPosition;
|
||||
use galaxyui_core::text_layout::CaretPosition;
|
||||
let mut carets = Vec::with_capacity(char_count);
|
||||
let char_width = if char_count > 0 {
|
||||
line_width / char_count as f32
|
||||
|
||||
@@ -1,26 +1,20 @@
|
||||
use crate::{
|
||||
content::text::BufferBlockStyle,
|
||||
editor::EditorView,
|
||||
extract_block,
|
||||
render::model::{BlockItem, RenderState, RichTextStyles, bounds, viewport::ViewportItem},
|
||||
};
|
||||
use galaxyui::elements::ListIndentLevel;
|
||||
use galaxyui::{
|
||||
AppContext, Element, SizeConstraint, WeakViewHandle,
|
||||
elements::{
|
||||
Align, Border, ConstrainedBox, Container, CornerRadius, Hoverable, Icon, MouseStateHandle,
|
||||
Radius, Rect,
|
||||
},
|
||||
geometry::vector::vec2f,
|
||||
platform::Cursor,
|
||||
};
|
||||
use pathfinder_color::ColorU;
|
||||
|
||||
use super::{
|
||||
RenderableBlock, RichTextAction,
|
||||
paint::RenderContext,
|
||||
placeholder::{self, BlockPlaceholder},
|
||||
use galaxyui_core::elements::{
|
||||
Align, Border, ConstrainedBox, Container, CornerRadius, Hoverable, Icon, ListIndentLevel,
|
||||
MouseStateHandle, Radius, Rect,
|
||||
};
|
||||
use galaxyui_core::geometry::vector::vec2f;
|
||||
use galaxyui_core::platform::Cursor;
|
||||
use galaxyui_core::{AppContext, Element, SizeConstraint, WeakViewHandle};
|
||||
|
||||
use super::paint::RenderContext;
|
||||
use super::placeholder::{self, BlockPlaceholder};
|
||||
use super::{RenderableBlock, RichTextAction};
|
||||
use crate::content::text::BufferBlockStyle;
|
||||
use crate::editor::EditorView;
|
||||
use crate::extract_block;
|
||||
use crate::render::model::viewport::ViewportItem;
|
||||
use crate::render::model::{BlockItem, RenderState, RichTextStyles, bounds};
|
||||
|
||||
// Minimum size constraint for the checkbox point. If the size is smaller than the constraint,
|
||||
// the svg won't render.
|
||||
@@ -112,8 +106,8 @@ impl RenderableBlock for RenderableTaskList {
|
||||
fn layout(
|
||||
&mut self,
|
||||
model: &RenderState,
|
||||
ctx: &mut galaxyui::LayoutContext,
|
||||
app: &galaxyui::AppContext,
|
||||
ctx: &mut galaxyui_core::LayoutContext,
|
||||
app: &galaxyui_core::AppContext,
|
||||
) {
|
||||
self.task_list_icon.layout(
|
||||
SizeConstraint::strict(vec2f(self.icon_size, self.icon_size)),
|
||||
@@ -142,7 +136,12 @@ impl RenderableBlock for RenderableTaskList {
|
||||
})
|
||||
}
|
||||
|
||||
fn paint(&mut self, model: &RenderState, ctx: &mut RenderContext, app: &galaxyui::AppContext) {
|
||||
fn paint(
|
||||
&mut self,
|
||||
model: &RenderState,
|
||||
ctx: &mut RenderContext,
|
||||
app: &galaxyui_core::AppContext,
|
||||
) {
|
||||
let content = model.content();
|
||||
let task_list = extract_block!(self.viewport_item, content, (block, BlockItem::TaskList{ paragraph: inner, ..}) => block.task_list(inner));
|
||||
let text_styling = &model.styles().base_text;
|
||||
@@ -174,8 +173,8 @@ impl RenderableBlock for RenderableTaskList {
|
||||
fn dispatch_event(
|
||||
&mut self,
|
||||
_model: &crate::render::model::RenderState,
|
||||
event: &galaxyui::event::DispatchedEvent,
|
||||
ctx: &mut galaxyui::EventContext,
|
||||
event: &galaxyui_core::event::DispatchedEvent,
|
||||
ctx: &mut galaxyui_core::EventContext,
|
||||
app: &AppContext,
|
||||
) -> bool {
|
||||
self.task_list_icon.dispatch_event(event, ctx, app)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use galaxy_core::ui::theme::Fill;
|
||||
|
||||
use crate::render::model::{BlockItem, Decoration, RenderState, viewport::ViewportItem};
|
||||
|
||||
use super::{RenderableBlock, paint::RenderContext};
|
||||
use super::RenderableBlock;
|
||||
use super::paint::RenderContext;
|
||||
use crate::render::model::viewport::ViewportItem;
|
||||
use crate::render::model::{BlockItem, Decoration, RenderState};
|
||||
|
||||
pub struct RenderableTemporaryBlock {
|
||||
viewport_item: ViewportItem,
|
||||
@@ -36,12 +37,17 @@ impl RenderableBlock for RenderableTemporaryBlock {
|
||||
fn layout(
|
||||
&mut self,
|
||||
_model: &RenderState,
|
||||
_ctx: &mut galaxyui::LayoutContext,
|
||||
_app: &galaxyui::AppContext,
|
||||
_ctx: &mut galaxyui_core::LayoutContext,
|
||||
_app: &galaxyui_core::AppContext,
|
||||
) {
|
||||
}
|
||||
|
||||
fn paint(&mut self, model: &RenderState, ctx: &mut RenderContext, _app: &galaxyui::AppContext) {
|
||||
fn paint(
|
||||
&mut self,
|
||||
model: &RenderState,
|
||||
ctx: &mut RenderContext,
|
||||
_app: &galaxyui_core::AppContext,
|
||||
) {
|
||||
// We cannot use `extract_block` macro here since we need to locate the viewport item by content height instead of charoffset
|
||||
// (temporary block has an offset of zero).
|
||||
let content = model.content();
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use crate::{
|
||||
extract_block,
|
||||
render::model::{BlockItem, RenderState, viewport::ViewportItem},
|
||||
};
|
||||
|
||||
use super::{RenderableBlock, paint::RenderContext};
|
||||
use super::RenderableBlock;
|
||||
use super::paint::RenderContext;
|
||||
use crate::extract_block;
|
||||
use crate::render::model::viewport::ViewportItem;
|
||||
use crate::render::model::{BlockItem, RenderState};
|
||||
|
||||
pub struct RenderableTextBlock {
|
||||
viewport_item: ViewportItem,
|
||||
@@ -23,12 +22,17 @@ impl RenderableBlock for RenderableTextBlock {
|
||||
fn layout(
|
||||
&mut self,
|
||||
_model: &RenderState,
|
||||
_ctx: &mut galaxyui::LayoutContext,
|
||||
_app: &galaxyui::AppContext,
|
||||
_ctx: &mut galaxyui_core::LayoutContext,
|
||||
_app: &galaxyui_core::AppContext,
|
||||
) {
|
||||
}
|
||||
|
||||
fn paint(&mut self, model: &RenderState, ctx: &mut RenderContext, _app: &galaxyui::AppContext) {
|
||||
fn paint(
|
||||
&mut self,
|
||||
model: &RenderState,
|
||||
ctx: &mut RenderContext,
|
||||
_app: &galaxyui_core::AppContext,
|
||||
) {
|
||||
let content = model.content();
|
||||
let text_block = extract_block!(
|
||||
self.viewport_item,
|
||||
|
||||
@@ -1,20 +1,14 @@
|
||||
use crate::{
|
||||
content::text::BufferBlockStyle,
|
||||
extract_block,
|
||||
render::model::{BlockItem, RenderState, RichTextStyles, bounds, viewport::ViewportItem},
|
||||
};
|
||||
use galaxyui::elements::ListIndentLevel;
|
||||
use galaxyui::{
|
||||
Element, SizeConstraint,
|
||||
elements::{Border, CornerRadius, Radius, Rect},
|
||||
geometry::vector::vec2f,
|
||||
};
|
||||
use galaxyui_core::elements::{Border, CornerRadius, ListIndentLevel, Radius, Rect};
|
||||
use galaxyui_core::geometry::vector::vec2f;
|
||||
use galaxyui_core::{Element, SizeConstraint};
|
||||
|
||||
use super::{
|
||||
RenderableBlock,
|
||||
paint::RenderContext,
|
||||
placeholder::{self, BlockPlaceholder},
|
||||
};
|
||||
use super::RenderableBlock;
|
||||
use super::paint::RenderContext;
|
||||
use super::placeholder::{self, BlockPlaceholder};
|
||||
use crate::content::text::BufferBlockStyle;
|
||||
use crate::extract_block;
|
||||
use crate::render::model::viewport::ViewportItem;
|
||||
use crate::render::model::{BlockItem, RenderState, RichTextStyles, bounds};
|
||||
|
||||
// Minimum size constraint for the bullet point. If the size is smaller than the constraint,
|
||||
// the svg won't render.
|
||||
@@ -67,8 +61,8 @@ impl RenderableBlock for RenderableBulletList {
|
||||
fn layout(
|
||||
&mut self,
|
||||
model: &RenderState,
|
||||
ctx: &mut galaxyui::LayoutContext,
|
||||
app: &galaxyui::AppContext,
|
||||
ctx: &mut galaxyui_core::LayoutContext,
|
||||
app: &galaxyui_core::AppContext,
|
||||
) {
|
||||
self.bullet_point.layout(
|
||||
SizeConstraint::strict(vec2f(self.bullet_size, self.bullet_size)),
|
||||
@@ -88,7 +82,12 @@ impl RenderableBlock for RenderableBulletList {
|
||||
})
|
||||
}
|
||||
|
||||
fn paint(&mut self, model: &RenderState, ctx: &mut RenderContext, app: &galaxyui::AppContext) {
|
||||
fn paint(
|
||||
&mut self,
|
||||
model: &RenderState,
|
||||
ctx: &mut RenderContext,
|
||||
app: &galaxyui_core::AppContext,
|
||||
) {
|
||||
let content = model.content();
|
||||
let unordered_list = extract_block!(self.viewport_item, content, (block, BlockItem::UnorderedList{ paragraph: inner, ..}) => block.unordered_list(inner));
|
||||
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
//! Shared text-layout utilities needed throughout the editor implementation.
|
||||
|
||||
#[cfg(test)]
|
||||
use markdown_parser::FormattedTextInline;
|
||||
use std::ops::Range;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::content::text::{BufferBlockStyle, TextStylesWithMetadata};
|
||||
use galaxyui::fonts::TextLayoutSystem;
|
||||
#[cfg(test)]
|
||||
use galaxyui::fonts::{Style, Weight};
|
||||
use galaxyui::text_layout::{
|
||||
ClipConfig, LayoutCache, Line, StyleAndFont, TextAlignment, TextBorder, TextStyle,
|
||||
use markdown_parser::FormattedTextInline;
|
||||
use galaxyui_core::color::ColorU;
|
||||
use galaxyui_core::fonts::TextLayoutSystem;
|
||||
#[cfg(test)]
|
||||
use galaxyui_core::fonts::{Style, Weight};
|
||||
use galaxyui_core::text_layout::{
|
||||
ClipConfig, LayoutCache, Line, StyleAndFont, TextAlignment, TextBorder, TextFrame, TextStyle,
|
||||
};
|
||||
use galaxyui::units::{IntoPixels, Pixels};
|
||||
use galaxyui::{AppContext, LayoutContext};
|
||||
use galaxyui::{color::ColorU, text_layout::TextFrame};
|
||||
use galaxyui_core::units::{IntoPixels, Pixels};
|
||||
use galaxyui_core::{AppContext, LayoutContext};
|
||||
|
||||
use super::model::{BlockSpacing, ParagraphStyles, RenderState, RichTextStyles};
|
||||
use crate::content::text::{BufferBlockStyle, TextStylesWithMetadata};
|
||||
|
||||
const HYPERLINK_UNDERLINE_COLOR: u32 = 0x7aa6daff;
|
||||
|
||||
|
||||
@@ -1,24 +1,18 @@
|
||||
//! End-to-end editor tests.
|
||||
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use galaxyui::{App, ModelHandle, ReadModel};
|
||||
|
||||
use crate::content::{
|
||||
buffer::{
|
||||
AutoScrollBehavior, Buffer, BufferEditAction, BufferEvent, BufferSelectAction, EditOrigin,
|
||||
InitialBufferState,
|
||||
},
|
||||
selection_model::BufferSelectionModel,
|
||||
text::{BlockType, BufferBlockItem, IndentBehavior, TextStyles},
|
||||
version::BufferVersion,
|
||||
};
|
||||
use string_offset::CharOffset;
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use galaxyui_core::{App, ModelHandle, ReadModel};
|
||||
|
||||
use super::model::{
|
||||
BlockItem, RenderEvent, RenderState,
|
||||
test_utils::{TEST_STYLES, init_logging},
|
||||
use super::model::test_utils::{TEST_STYLES, init_logging};
|
||||
use super::model::{BlockItem, RenderEvent, RenderState};
|
||||
use crate::content::buffer::{
|
||||
AutoScrollBehavior, Buffer, BufferEditAction, BufferEvent, BufferSelectAction, EditOrigin,
|
||||
InitialBufferState, ShouldAutoscroll,
|
||||
};
|
||||
use crate::content::buffer::ShouldAutoscroll;
|
||||
use crate::content::selection_model::BufferSelectionModel;
|
||||
use crate::content::text::{BlockType, BufferBlockItem, IndentBehavior, TextStyles};
|
||||
use crate::content::version::BufferVersion;
|
||||
|
||||
#[test]
|
||||
fn test_simple_edit() {
|
||||
@@ -44,7 +38,7 @@ fn test_simple_edit() {
|
||||
&app,
|
||||
r#"
|
||||
-------- 0.00px / 0 characters --------
|
||||
Paragraph (2 characters, 1 lines, 32.00px tall)
|
||||
Paragraph (2 characters, 1 lines, 24.00px tall)
|
||||
"#,
|
||||
);
|
||||
});
|
||||
@@ -94,57 +88,57 @@ zzzzzzzzzzzzzzzzzzzzzzzzzz"#,
|
||||
&app,
|
||||
r#"
|
||||
-------- 0.00px / 0 characters --------
|
||||
Paragraph (2 characters, 1 lines, 32.00px tall)
|
||||
-------- 32.00px / 2 characters --------
|
||||
Paragraph (3 characters, 1 lines, 32.00px tall)
|
||||
-------- 64.00px / 5 characters --------
|
||||
Paragraph (4 characters, 1 lines, 32.00px tall)
|
||||
-------- 96.00px / 9 characters --------
|
||||
Paragraph (5 characters, 1 lines, 32.00px tall)
|
||||
-------- 128.00px / 14 characters --------
|
||||
Paragraph (6 characters, 1 lines, 32.00px tall)
|
||||
-------- 160.00px / 20 characters --------
|
||||
Paragraph (7 characters, 1 lines, 32.00px tall)
|
||||
-------- 192.00px / 27 characters --------
|
||||
Paragraph (8 characters, 1 lines, 32.00px tall)
|
||||
-------- 224.00px / 35 characters --------
|
||||
Paragraph (9 characters, 1 lines, 32.00px tall)
|
||||
-------- 256.00px / 44 characters --------
|
||||
Paragraph (10 characters, 1 lines, 32.00px tall)
|
||||
-------- 288.00px / 54 characters --------
|
||||
Paragraph (11 characters, 1 lines, 32.00px tall)
|
||||
-------- 320.00px / 65 characters --------
|
||||
Paragraph (12 characters, 1 lines, 32.00px tall)
|
||||
-------- 352.00px / 77 characters --------
|
||||
Paragraph (13 characters, 1 lines, 32.00px tall)
|
||||
-------- 384.00px / 90 characters --------
|
||||
Paragraph (14 characters, 1 lines, 32.00px tall)
|
||||
-------- 416.00px / 104 characters --------
|
||||
Paragraph (15 characters, 1 lines, 32.00px tall)
|
||||
-------- 448.00px / 119 characters --------
|
||||
Paragraph (16 characters, 1 lines, 32.00px tall)
|
||||
-------- 480.00px / 135 characters --------
|
||||
Paragraph (17 characters, 1 lines, 32.00px tall)
|
||||
-------- 512.00px / 152 characters --------
|
||||
Paragraph (18 characters, 1 lines, 32.00px tall)
|
||||
-------- 544.00px / 170 characters --------
|
||||
Paragraph (19 characters, 1 lines, 32.00px tall)
|
||||
-------- 576.00px / 189 characters --------
|
||||
Paragraph (20 characters, 1 lines, 32.00px tall)
|
||||
-------- 608.00px / 209 characters --------
|
||||
Paragraph (21 characters, 1 lines, 32.00px tall)
|
||||
-------- 640.00px / 230 characters --------
|
||||
Paragraph (22 characters, 1 lines, 32.00px tall)
|
||||
-------- 672.00px / 252 characters --------
|
||||
Paragraph (23 characters, 1 lines, 32.00px tall)
|
||||
-------- 704.00px / 275 characters --------
|
||||
Paragraph (24 characters, 1 lines, 32.00px tall)
|
||||
-------- 736.00px / 299 characters --------
|
||||
Paragraph (25 characters, 1 lines, 32.00px tall)
|
||||
-------- 768.00px / 324 characters --------
|
||||
Paragraph (26 characters, 1 lines, 32.00px tall)
|
||||
-------- 800.00px / 350 characters --------
|
||||
Paragraph (27 characters, 1 lines, 32.00px tall)
|
||||
Paragraph (2 characters, 1 lines, 24.00px tall)
|
||||
-------- 24.00px / 2 characters --------
|
||||
Paragraph (3 characters, 1 lines, 24.00px tall)
|
||||
-------- 48.00px / 5 characters --------
|
||||
Paragraph (4 characters, 1 lines, 24.00px tall)
|
||||
-------- 72.00px / 9 characters --------
|
||||
Paragraph (5 characters, 1 lines, 24.00px tall)
|
||||
-------- 96.00px / 14 characters --------
|
||||
Paragraph (6 characters, 1 lines, 24.00px tall)
|
||||
-------- 120.00px / 20 characters --------
|
||||
Paragraph (7 characters, 1 lines, 24.00px tall)
|
||||
-------- 144.00px / 27 characters --------
|
||||
Paragraph (8 characters, 1 lines, 24.00px tall)
|
||||
-------- 168.00px / 35 characters --------
|
||||
Paragraph (9 characters, 1 lines, 24.00px tall)
|
||||
-------- 192.00px / 44 characters --------
|
||||
Paragraph (10 characters, 1 lines, 24.00px tall)
|
||||
-------- 216.00px / 54 characters --------
|
||||
Paragraph (11 characters, 1 lines, 24.00px tall)
|
||||
-------- 240.00px / 65 characters --------
|
||||
Paragraph (12 characters, 1 lines, 24.00px tall)
|
||||
-------- 264.00px / 77 characters --------
|
||||
Paragraph (13 characters, 1 lines, 24.00px tall)
|
||||
-------- 288.00px / 90 characters --------
|
||||
Paragraph (14 characters, 1 lines, 24.00px tall)
|
||||
-------- 312.00px / 104 characters --------
|
||||
Paragraph (15 characters, 1 lines, 24.00px tall)
|
||||
-------- 336.00px / 119 characters --------
|
||||
Paragraph (16 characters, 1 lines, 24.00px tall)
|
||||
-------- 360.00px / 135 characters --------
|
||||
Paragraph (17 characters, 1 lines, 24.00px tall)
|
||||
-------- 384.00px / 152 characters --------
|
||||
Paragraph (18 characters, 1 lines, 24.00px tall)
|
||||
-------- 408.00px / 170 characters --------
|
||||
Paragraph (19 characters, 1 lines, 24.00px tall)
|
||||
-------- 432.00px / 189 characters --------
|
||||
Paragraph (20 characters, 1 lines, 24.00px tall)
|
||||
-------- 456.00px / 209 characters --------
|
||||
Paragraph (21 characters, 1 lines, 24.00px tall)
|
||||
-------- 480.00px / 230 characters --------
|
||||
Paragraph (22 characters, 1 lines, 24.00px tall)
|
||||
-------- 504.00px / 252 characters --------
|
||||
Paragraph (23 characters, 1 lines, 24.00px tall)
|
||||
-------- 528.00px / 275 characters --------
|
||||
Paragraph (24 characters, 1 lines, 24.00px tall)
|
||||
-------- 552.00px / 299 characters --------
|
||||
Paragraph (25 characters, 1 lines, 24.00px tall)
|
||||
-------- 576.00px / 324 characters --------
|
||||
Paragraph (26 characters, 1 lines, 24.00px tall)
|
||||
-------- 600.00px / 350 characters --------
|
||||
Paragraph (27 characters, 1 lines, 24.00px tall)
|
||||
"#,
|
||||
);
|
||||
});
|
||||
@@ -173,13 +167,13 @@ fn test_enter_before_horizontal_rule() {
|
||||
app,
|
||||
r#"
|
||||
-------- 0.00px / 0 characters --------
|
||||
Paragraph (11 characters, 1 lines, 32.00px tall)
|
||||
-------- 32.00px / 11 characters --------
|
||||
Paragraph (1 characters, 1 lines, 32.00px tall)
|
||||
-------- 64.00px / 12 characters --------
|
||||
Horizontal Rule (1 characters, 1 lines, 18.00px tall)
|
||||
-------- 82.00px / 13 characters --------
|
||||
Paragraph (12 characters, 1 lines, 32.00px tall)
|
||||
Paragraph (11 characters, 1 lines, 24.00px tall)
|
||||
-------- 24.00px / 11 characters --------
|
||||
Paragraph (1 characters, 1 lines, 24.00px tall)
|
||||
-------- 48.00px / 12 characters --------
|
||||
Horizontal Rule (1 characters, 1 lines, 10.00px tall)
|
||||
-------- 58.00px / 13 characters --------
|
||||
Paragraph (12 characters, 1 lines, 24.00px tall)
|
||||
"#,
|
||||
);
|
||||
})
|
||||
@@ -208,13 +202,13 @@ fn test_enter_after_horizontal_rule() {
|
||||
app,
|
||||
r#"
|
||||
-------- 0.00px / 0 characters --------
|
||||
Paragraph (11 characters, 1 lines, 32.00px tall)
|
||||
-------- 32.00px / 11 characters --------
|
||||
Horizontal Rule (1 characters, 1 lines, 18.00px tall)
|
||||
-------- 50.00px / 12 characters --------
|
||||
Paragraph (1 characters, 1 lines, 32.00px tall)
|
||||
-------- 82.00px / 13 characters --------
|
||||
Paragraph (12 characters, 1 lines, 32.00px tall)
|
||||
Paragraph (11 characters, 1 lines, 24.00px tall)
|
||||
-------- 24.00px / 11 characters --------
|
||||
Horizontal Rule (1 characters, 1 lines, 10.00px tall)
|
||||
-------- 34.00px / 12 characters --------
|
||||
Paragraph (1 characters, 1 lines, 24.00px tall)
|
||||
-------- 58.00px / 13 characters --------
|
||||
Paragraph (12 characters, 1 lines, 24.00px tall)
|
||||
"#,
|
||||
);
|
||||
})
|
||||
@@ -244,13 +238,13 @@ fn test_edit_at_horizontal_rule_end() {
|
||||
app,
|
||||
r#"
|
||||
-------- 0.00px / 0 characters --------
|
||||
Paragraph (11 characters, 1 lines, 32.00px tall)
|
||||
-------- 32.00px / 11 characters --------
|
||||
Horizontal Rule (1 characters, 1 lines, 18.00px tall)
|
||||
-------- 50.00px / 12 characters --------
|
||||
Paragraph (2 characters, 1 lines, 32.00px tall)
|
||||
-------- 82.00px / 14 characters --------
|
||||
Paragraph (12 characters, 1 lines, 32.00px tall)
|
||||
Paragraph (11 characters, 1 lines, 24.00px tall)
|
||||
-------- 24.00px / 11 characters --------
|
||||
Horizontal Rule (1 characters, 1 lines, 10.00px tall)
|
||||
-------- 34.00px / 12 characters --------
|
||||
Paragraph (2 characters, 1 lines, 24.00px tall)
|
||||
-------- 58.00px / 14 characters --------
|
||||
Paragraph (12 characters, 1 lines, 24.00px tall)
|
||||
"#,
|
||||
);
|
||||
})
|
||||
@@ -286,9 +280,9 @@ fn test_edit_after_style() {
|
||||
app,
|
||||
r#"
|
||||
-------- 0.00px / 0 characters --------
|
||||
Paragraph (18 characters, 1 lines, 32.00px tall)
|
||||
-------- 32.00px / 18 characters --------
|
||||
Paragraph (9 characters, 1 lines, 32.00px tall)
|
||||
Paragraph (18 characters, 1 lines, 24.00px tall)
|
||||
-------- 24.00px / 18 characters --------
|
||||
Paragraph (9 characters, 1 lines, 24.00px tall)
|
||||
"#,
|
||||
);
|
||||
})
|
||||
@@ -323,7 +317,7 @@ Task List @ 1 [X] (2 characters, 1 lines, 18.00px tall)
|
||||
-------- 18.00px / 2 characters --------
|
||||
Task List @ 1 [ ] (2 characters, 1 lines, 18.00px tall)
|
||||
-------- 36.00px / 4 characters --------
|
||||
Trailing Newline (1 characters, 1 lines, 32.00px tall)
|
||||
Trailing Newline (1 characters, 1 lines, 24.00px tall)
|
||||
"#,
|
||||
);
|
||||
|
||||
@@ -344,7 +338,7 @@ Task List @ 1 [ ] (2 characters, 1 lines, 18.00px tall)
|
||||
-------- 54.00px / 6 characters --------
|
||||
Task List @ 1 [ ] (2 characters, 1 lines, 18.00px tall)
|
||||
-------- 72.00px / 8 characters --------
|
||||
Trailing Newline (1 characters, 1 lines, 32.00px tall)
|
||||
Trailing Newline (1 characters, 1 lines, 24.00px tall)
|
||||
"#,
|
||||
)
|
||||
});
|
||||
@@ -377,11 +371,11 @@ fn test_convert_first_line() {
|
||||
app,
|
||||
r#"
|
||||
-------- 0.00px / 0 characters --------
|
||||
Horizontal Rule (1 characters, 1 lines, 18.00px tall)
|
||||
-------- 18.00px / 1 characters --------
|
||||
Horizontal Rule (1 characters, 1 lines, 10.00px tall)
|
||||
-------- 10.00px / 1 characters --------
|
||||
Code Block - Shell (5 characters, 1 lines, 84.00px tall)
|
||||
-------- 102.00px / 6 characters --------
|
||||
Trailing Newline (1 characters, 1 lines, 32.00px tall)
|
||||
-------- 94.00px / 6 characters --------
|
||||
Trailing Newline (1 characters, 1 lines, 24.00px tall)
|
||||
"#,
|
||||
);
|
||||
|
||||
@@ -394,11 +388,11 @@ Trailing Newline (1 characters, 1 lines, 32.00px tall)
|
||||
app,
|
||||
r#"
|
||||
-------- 0.00px / 0 characters --------
|
||||
Paragraph (3 characters, 1 lines, 32.00px tall)
|
||||
-------- 32.00px / 3 characters --------
|
||||
Paragraph (3 characters, 1 lines, 24.00px tall)
|
||||
-------- 24.00px / 3 characters --------
|
||||
Code Block - Shell (5 characters, 1 lines, 84.00px tall)
|
||||
-------- 116.00px / 8 characters --------
|
||||
Trailing Newline (1 characters, 1 lines, 32.00px tall)
|
||||
-------- 108.00px / 8 characters --------
|
||||
Trailing Newline (1 characters, 1 lines, 24.00px tall)
|
||||
"#,
|
||||
)
|
||||
});
|
||||
|
||||
@@ -9,13 +9,9 @@
|
||||
//! * The **reserved box** is the rectangle containing a block's content, padding, borders, and
|
||||
//! margin - all space reserved for the block.
|
||||
|
||||
use galaxyui::{
|
||||
geometry::{
|
||||
rect::RectF,
|
||||
vector::{Vector2F, vec2f},
|
||||
},
|
||||
units::Pixels,
|
||||
};
|
||||
use galaxyui_core::geometry::rect::RectF;
|
||||
use galaxyui_core::geometry::vector::{Vector2F, vec2f};
|
||||
use galaxyui_core::units::Pixels;
|
||||
|
||||
use super::BlockSpacing;
|
||||
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
use galaxyui::units::{IntoPixels, Pixels};
|
||||
use num_traits::SaturatingSub;
|
||||
use sum_tree::SeekBias;
|
||||
|
||||
use string_offset::CharOffset;
|
||||
use sum_tree::SeekBias;
|
||||
use galaxyui_core::units::{IntoPixels, Pixels};
|
||||
|
||||
use super::positioned::{Positioned, PositionedCursor};
|
||||
use super::{
|
||||
BlockItem, Height, HitTestBlockType, LayoutSummary, ParagraphBlock, RenderState, bounds,
|
||||
positioned::{Positioned, PositionedCursor},
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
use crate::content::text::{FormattedTable, table_cell_offset_maps};
|
||||
use crate::{
|
||||
content::text::{BufferBlockStyle, CodeBlockType},
|
||||
render::model::{
|
||||
BlockItem, COMMAND_SPACING, CellLayout, ImageBlockConfig, LaidOutTable, Location,
|
||||
ParagraphBlock, RenderState, TableBlockConfig, TableStyle,
|
||||
location::{HitTestBlockType, HitTestOptions, WrapDirection},
|
||||
table_offset_map,
|
||||
test_utils::{
|
||||
TEST_STYLES, laid_out_paragraph, laid_out_unordered_lists, layout_paragraphs,
|
||||
},
|
||||
},
|
||||
};
|
||||
use pathfinder_color::ColorU;
|
||||
use std::{cell::Cell, sync::Arc};
|
||||
use string_offset::CharOffset;
|
||||
use std::cell::Cell;
|
||||
use std::sync::Arc;
|
||||
|
||||
use galaxyui::assets::asset_cache::AssetSource;
|
||||
use galaxyui::fonts::FamilyId;
|
||||
use galaxyui::text_layout::{CaretPosition, TextFrame};
|
||||
use galaxyui::units::IntoPixels;
|
||||
use pathfinder_color::ColorU;
|
||||
use string_offset::CharOffset;
|
||||
use sum_tree::SumTree;
|
||||
use galaxyui_core::assets::asset_cache::AssetSource;
|
||||
use galaxyui_core::fonts::FamilyId;
|
||||
use galaxyui_core::text_layout::{CaretPosition, TextFrame};
|
||||
use galaxyui_core::units::IntoPixels;
|
||||
|
||||
use crate::content::text::{
|
||||
BufferBlockStyle, CodeBlockType, FormattedTable, table_cell_offset_maps,
|
||||
};
|
||||
use crate::render::model::location::{HitTestBlockType, HitTestOptions, WrapDirection};
|
||||
use crate::render::model::test_utils::{
|
||||
TEST_STYLES, laid_out_paragraph, laid_out_unordered_lists, layout_paragraphs,
|
||||
};
|
||||
use crate::render::model::{
|
||||
BlockItem, COMMAND_SPACING, CellLayout, ImageBlockConfig, LaidOutTable, Location,
|
||||
ParagraphBlock, RenderState, TableBlockConfig, TableStyle, table_offset_map,
|
||||
};
|
||||
|
||||
fn test_table_layout() -> LaidOutTable {
|
||||
let source = "aaa\tbbb\nccc\tddd\n";
|
||||
@@ -493,10 +493,10 @@ fn test_hit_scrolled() {
|
||||
&Default::default()
|
||||
),
|
||||
Location::Text {
|
||||
char_offset: 15.into(),
|
||||
clamped: true,
|
||||
wrap_direction: WrapDirection::Up,
|
||||
block_start: 8.into(),
|
||||
char_offset: 19.into(),
|
||||
clamped: false,
|
||||
wrap_direction: WrapDirection::Down,
|
||||
block_start: 16.into(),
|
||||
link: None,
|
||||
}
|
||||
);
|
||||
@@ -510,10 +510,10 @@ fn test_hit_scrolled() {
|
||||
&Default::default()
|
||||
),
|
||||
Location::Text {
|
||||
char_offset: 16.into(),
|
||||
clamped: false,
|
||||
char_offset: 21.into(),
|
||||
clamped: true,
|
||||
wrap_direction: WrapDirection::Down,
|
||||
block_start: 16.into(),
|
||||
block_start: 21.into(),
|
||||
link: None,
|
||||
}
|
||||
);
|
||||
@@ -580,23 +580,24 @@ fn test_hit_code_block() {
|
||||
width - COMMAND_SPACING.x_axis_offset().as_f32(),
|
||||
)),
|
||||
code_block_type: Default::default(),
|
||||
pending_mermaid_asset: None,
|
||||
},
|
||||
]);
|
||||
model.set_content(tree);
|
||||
|
||||
// Blocks by height:
|
||||
// * 0-32: First paragraph
|
||||
// * 32-56: Margin above code block
|
||||
// * 56-66: First line of code
|
||||
// * 66-76: Second line of code
|
||||
// * 76-92: Margin below code block
|
||||
// * 0-24: First paragraph
|
||||
// * 24-48: Margin above code block
|
||||
// * 48-58: First line of code
|
||||
// * 58-68: Second line of code
|
||||
// * 68-84: Margin below code block
|
||||
// The code block is inset by 16px.
|
||||
|
||||
// Hits within the code block should have the right start location.
|
||||
assert_eq!(
|
||||
model.render_coordinates_to_location(
|
||||
30.0.into_pixels(),
|
||||
70.0.into_pixels(),
|
||||
62.0.into_pixels(),
|
||||
&Default::default()
|
||||
),
|
||||
Location::Text {
|
||||
@@ -614,7 +615,7 @@ fn test_hit_code_block() {
|
||||
assert_eq!(
|
||||
model.render_coordinates_to_location(
|
||||
10.0.into_pixels(),
|
||||
50.0.into_pixels(),
|
||||
40.0.into_pixels(),
|
||||
&Default::default()
|
||||
),
|
||||
Location::Block {
|
||||
@@ -628,7 +629,7 @@ fn test_hit_code_block() {
|
||||
assert_eq!(
|
||||
model.render_coordinates_to_location(
|
||||
8.0.into_pixels(),
|
||||
60.0.into_pixels(),
|
||||
52.0.into_pixels(),
|
||||
&Default::default()
|
||||
),
|
||||
Location::Text {
|
||||
@@ -642,7 +643,7 @@ fn test_hit_code_block() {
|
||||
assert_eq!(
|
||||
model.render_coordinates_to_location(
|
||||
90.0.into_pixels(),
|
||||
60.0.into_pixels(),
|
||||
52.0.into_pixels(),
|
||||
&Default::default()
|
||||
),
|
||||
Location::Text {
|
||||
@@ -658,7 +659,7 @@ fn test_hit_code_block() {
|
||||
assert_eq!(
|
||||
model.render_coordinates_to_location(
|
||||
(-4.).into_pixels(),
|
||||
60.0.into_pixels(),
|
||||
52.0.into_pixels(),
|
||||
&Default::default()
|
||||
),
|
||||
Location::Text {
|
||||
@@ -672,7 +673,7 @@ fn test_hit_code_block() {
|
||||
assert_eq!(
|
||||
model.render_coordinates_to_location(
|
||||
1000.0.into_pixels(),
|
||||
60.0.into_pixels(),
|
||||
52.0.into_pixels(),
|
||||
&Default::default()
|
||||
),
|
||||
Location::Text {
|
||||
@@ -688,7 +689,7 @@ fn test_hit_code_block() {
|
||||
assert_eq!(
|
||||
model.render_coordinates_to_location(
|
||||
27.0.into_pixels(),
|
||||
50.0.into_pixels(),
|
||||
40.0.into_pixels(),
|
||||
&HitTestOptions {
|
||||
force_text_selection: true
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,80 +1,75 @@
|
||||
use galaxyui::assets::asset_cache::AssetSource;
|
||||
use galaxyui::{
|
||||
color::ColorU,
|
||||
fonts::FamilyId,
|
||||
geometry::{rect::RectF, vector::vec2f},
|
||||
text_layout::TextFrame,
|
||||
units::{IntoPixels, Pixels},
|
||||
};
|
||||
use std::cell::Cell;
|
||||
use std::sync::Arc;
|
||||
|
||||
use markdown_parser::{FormattedTextStyles, Hyperlink};
|
||||
use rangemap::RangeSet;
|
||||
use std::{cell::Cell, sync::Arc};
|
||||
use string_offset::CharOffset;
|
||||
use sum_tree::SumTree;
|
||||
use vec1::{Vec1, vec1};
|
||||
use galaxyui_core::assets::asset_cache::AssetSource;
|
||||
use galaxyui_core::color::ColorU;
|
||||
use galaxyui_core::elements::ListIndentLevel;
|
||||
use galaxyui_core::fonts::FamilyId;
|
||||
use galaxyui_core::geometry::rect::RectF;
|
||||
use galaxyui_core::geometry::vector::vec2f;
|
||||
use galaxyui_core::text_layout::TextFrame;
|
||||
use galaxyui_core::units::{IntoPixels, Pixels};
|
||||
|
||||
use super::debug::Describe;
|
||||
use super::test_utils::{layout_paragraph, layout_paragraphs};
|
||||
use super::{
|
||||
BlockItem, BlockLocation, COMMAND_SPACING, CellLayout, DEFAULT_BLOCK_SPACINGS,
|
||||
HiddenBlockConfig, ImageBlockConfig, LaidOutTable, ParagraphBlock, RenderState,
|
||||
TableBlockConfig, TableStyle,
|
||||
debug::Describe,
|
||||
table_offset_map,
|
||||
test_utils::{layout_paragraph, layout_paragraphs},
|
||||
TableBlockConfig, TableStyle, table_offset_map,
|
||||
};
|
||||
use crate::{
|
||||
content::{
|
||||
edit::ParsedUrl,
|
||||
text::{
|
||||
BufferBlockStyle, CodeBlockType, FormattedTable, FormattedTextFragment,
|
||||
table_cell_offset_maps,
|
||||
},
|
||||
},
|
||||
render::model::{
|
||||
Height, LayoutSummary, LineCount, RenderedSelection, SoftWrapPoint, TEXT_SPACING,
|
||||
test_utils::{TEST_STYLES, laid_out_paragraph, mock_paragraph},
|
||||
},
|
||||
use crate::content::edit::ParsedUrl;
|
||||
use crate::content::text::{
|
||||
BufferBlockStyle, CodeBlockType, FormattedTable, FormattedTextFragment, table_cell_offset_maps,
|
||||
};
|
||||
use crate::render::model::test_utils::{TEST_STYLES, laid_out_paragraph, mock_paragraph};
|
||||
use crate::render::model::{
|
||||
ColumnUnit, Height, LayoutSummary, LineCount, RenderedSelection, SoftWrapPoint, TEXT_SPACING,
|
||||
};
|
||||
use galaxyui::elements::ListIndentLevel;
|
||||
use markdown_parser::{FormattedTextStyles, Hyperlink};
|
||||
use string_offset::CharOffset;
|
||||
|
||||
#[test]
|
||||
fn test_height() {
|
||||
let mut render_state =
|
||||
RenderState::new_for_test(TEST_STYLES, 10.0.into_pixels(), 10.0.into_pixels());
|
||||
let mut content = SumTree::new();
|
||||
// Height: 32
|
||||
// Height: 24
|
||||
content.push(mock_paragraph(24., 1., 1));
|
||||
// Height: 56
|
||||
// Height: 48
|
||||
content.push(mock_paragraph(48., 1., 2));
|
||||
// Height: 32
|
||||
// Height: 24
|
||||
content.push(mock_paragraph(24., 1., 3));
|
||||
// Height: 32
|
||||
// Height: 24
|
||||
content.push(mock_paragraph(24., 1., 4));
|
||||
// Height: 40
|
||||
// Height: 32
|
||||
content.push(mock_paragraph(32., 1., 5));
|
||||
render_state.set_content(content);
|
||||
|
||||
// This includes all content plus the trailing newline marker.
|
||||
assert_eq!(render_state.height(), 224.0.into_pixels());
|
||||
assert_eq!(render_state.height(), 176.0.into_pixels());
|
||||
let content = render_state.content.borrow();
|
||||
let mut cursor = content.cursor::<Height, Height>();
|
||||
// Ensure we can seek in between items for scrolling.
|
||||
cursor.seek(&Height::from(64.), sum_tree::SeekBias::Left);
|
||||
assert_eq!(
|
||||
cursor.item().expect("Seek succeeded").height().as_f32(),
|
||||
56.
|
||||
48.
|
||||
);
|
||||
assert_eq!(cursor.start().into_pixels().as_f32(), 32.);
|
||||
assert_eq!(cursor.end().into_pixels().as_f32(), 88.);
|
||||
assert_eq!(cursor.start().into_pixels().as_f32(), 24.);
|
||||
assert_eq!(cursor.end().into_pixels().as_f32(), 72.);
|
||||
|
||||
let end = cursor.slice(&Height::from(152.), sum_tree::SeekBias::Right);
|
||||
assert_eq!(
|
||||
end.summary(),
|
||||
LayoutSummary {
|
||||
content_length: 9.into(),
|
||||
height: 56. + 32. + 32.,
|
||||
width: (21.).into_pixels(),
|
||||
lines: LineCount(3),
|
||||
item_count: 3,
|
||||
content_length: 14.into(),
|
||||
height: 48. + 24. + 24. + 32.,
|
||||
width: (17.).into_pixels(),
|
||||
lines: LineCount(4),
|
||||
item_count: 4,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -138,7 +133,7 @@ fn test_width() {
|
||||
render_state.set_content(content);
|
||||
|
||||
// This includes all content plus the trailing newline marker.
|
||||
assert_eq!(render_state.width(), (45.).into_pixels());
|
||||
assert_eq!(render_state.width(), (41.).into_pixels());
|
||||
let content = render_state.content.borrow();
|
||||
let mut cursor = content.cursor::<Height, Height>();
|
||||
let end = cursor.slice(&Height::from(40.), sum_tree::SeekBias::Right);
|
||||
@@ -146,8 +141,8 @@ fn test_width() {
|
||||
end.summary(),
|
||||
LayoutSummary {
|
||||
content_length: 1.into(),
|
||||
height: 32.,
|
||||
width: (30.).into_pixels(),
|
||||
height: 24.,
|
||||
width: (26.).into_pixels(),
|
||||
lines: LineCount(1),
|
||||
item_count: 1,
|
||||
}
|
||||
@@ -160,6 +155,10 @@ fn test_soft_wrap_point() {
|
||||
fn char_x(chars: usize) -> Pixels {
|
||||
TEXT_SPACING.left_offset() + (chars as f32 * TEST_STYLES.base_text.font_size).into_pixels()
|
||||
}
|
||||
/// Wraps a Pixels value as a ColumnUnit for constructing SoftWrapPoints in Pixels mode.
|
||||
fn px(p: Pixels) -> ColumnUnit {
|
||||
ColumnUnit::Pixels(p)
|
||||
}
|
||||
|
||||
let mut model =
|
||||
RenderState::new_for_test(TEST_STYLES.clone(), 40.0.into_pixels(), 60.0.into_pixels());
|
||||
@@ -181,82 +180,82 @@ fn test_soft_wrap_point() {
|
||||
// Last point on the first softwrapped line.
|
||||
assert_eq!(
|
||||
model.offset_to_softwrap_point(CharOffset::from(3)),
|
||||
SoftWrapPoint::new(0, char_x(3))
|
||||
SoftWrapPoint::new(0, px(char_x(3)))
|
||||
);
|
||||
|
||||
// A point slightly closer to 2 than 3 should round to 2.
|
||||
assert_eq!(
|
||||
model.softwrap_point_to_offset(SoftWrapPoint::new(0, char_x(2) + 4.0.into_pixels())),
|
||||
model.softwrap_point_to_offset(SoftWrapPoint::new(0, px(char_x(2) + 4.0.into_pixels()))),
|
||||
CharOffset::from(2)
|
||||
);
|
||||
|
||||
// A point slightly closer to 3 than 2 should round to 3.
|
||||
assert_eq!(
|
||||
model.softwrap_point_to_offset(SoftWrapPoint::new(0, char_x(3) - 4.0.into_pixels())),
|
||||
model.softwrap_point_to_offset(SoftWrapPoint::new(0, px(char_x(3) - 4.0.into_pixels()))),
|
||||
CharOffset::from(3)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
model.softwrap_point_to_offset(SoftWrapPoint::new(0, char_x(4))),
|
||||
model.softwrap_point_to_offset(SoftWrapPoint::new(0, px(char_x(4)))),
|
||||
CharOffset::from(4)
|
||||
);
|
||||
|
||||
// Point on the second softwrapped line in the first paragraph.
|
||||
assert_eq!(
|
||||
model.offset_to_softwrap_point(CharOffset::from(7)),
|
||||
SoftWrapPoint::new(1, char_x(3))
|
||||
SoftWrapPoint::new(1, px(char_x(3)))
|
||||
);
|
||||
assert_eq!(
|
||||
model.softwrap_point_to_offset(SoftWrapPoint::new(1, char_x(3))),
|
||||
model.softwrap_point_to_offset(SoftWrapPoint::new(1, px(char_x(3)))),
|
||||
CharOffset::from(7)
|
||||
);
|
||||
|
||||
// Non-softwrapped line should work as well.
|
||||
assert_eq!(
|
||||
model.offset_to_softwrap_point(CharOffset::from(10)),
|
||||
SoftWrapPoint::new(2, char_x(2))
|
||||
SoftWrapPoint::new(2, px(char_x(2)))
|
||||
);
|
||||
assert_eq!(
|
||||
model.softwrap_point_to_offset(SoftWrapPoint::new(2, char_x(2))),
|
||||
model.softwrap_point_to_offset(SoftWrapPoint::new(2, px(char_x(2)))),
|
||||
CharOffset::from(10)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
model.offset_to_softwrap_point(CharOffset::from(19)),
|
||||
SoftWrapPoint::new(4, char_x(2))
|
||||
SoftWrapPoint::new(4, px(char_x(2)))
|
||||
);
|
||||
assert_eq!(
|
||||
model.softwrap_point_to_offset(SoftWrapPoint::new(4, char_x(2))),
|
||||
model.softwrap_point_to_offset(SoftWrapPoint::new(4, px(char_x(2)))),
|
||||
CharOffset::from(19)
|
||||
);
|
||||
|
||||
// Softwrapping on an empty line should work.
|
||||
assert_eq!(
|
||||
model.offset_to_softwrap_point(CharOffset::from(21)),
|
||||
SoftWrapPoint::new(5, TEXT_SPACING.left_offset())
|
||||
SoftWrapPoint::new(5, px(TEXT_SPACING.left_offset()))
|
||||
);
|
||||
assert_eq!(
|
||||
model.softwrap_point_to_offset(SoftWrapPoint::new(5, Pixels::zero())),
|
||||
model.softwrap_point_to_offset(SoftWrapPoint::new(5, ColumnUnit::pixels_zero())),
|
||||
CharOffset::from(21)
|
||||
);
|
||||
|
||||
// Out of bound points should be bounded to the trailing newline.
|
||||
assert_eq!(
|
||||
model.offset_to_softwrap_point(CharOffset::from(40)),
|
||||
SoftWrapPoint::new(8, Pixels::zero())
|
||||
SoftWrapPoint::new(8, ColumnUnit::pixels_zero())
|
||||
);
|
||||
assert_eq!(
|
||||
model.softwrap_point_to_offset(SoftWrapPoint::new(7, Pixels::zero())),
|
||||
model.softwrap_point_to_offset(SoftWrapPoint::new(7, ColumnUnit::pixels_zero())),
|
||||
CharOffset::from(26)
|
||||
);
|
||||
|
||||
// Points are bounded to their line's contents.
|
||||
assert_eq!(
|
||||
model.softwrap_point_to_offset(SoftWrapPoint::new(5, char_x(3))),
|
||||
model.softwrap_point_to_offset(SoftWrapPoint::new(5, px(char_x(3)))),
|
||||
CharOffset::from(21)
|
||||
);
|
||||
assert_eq!(
|
||||
model.softwrap_point_to_offset(SoftWrapPoint::new(5, char_x(2))),
|
||||
model.softwrap_point_to_offset(SoftWrapPoint::new(5, px(char_x(2)))),
|
||||
CharOffset::from(21)
|
||||
);
|
||||
}
|
||||
@@ -280,39 +279,38 @@ fn test_character_bounds() {
|
||||
));
|
||||
model.set_content(content);
|
||||
|
||||
// Due to the minimum block height, there is 6px of top spacing. In addition, there's a 4px
|
||||
// left margin.
|
||||
// Due to the minimum block height, there is 2px of top spacing.
|
||||
|
||||
let char_size = vec2f(10., 10.);
|
||||
|
||||
// The middle of the first line.
|
||||
assert_eq!(
|
||||
model.character_bounds(2.into()),
|
||||
Some(RectF::new(vec2f(24., 6.), char_size))
|
||||
Some(RectF::new(vec2f(20., 2.), char_size))
|
||||
);
|
||||
|
||||
// The first character of the second soft-wrapped line.
|
||||
assert_eq!(
|
||||
model.character_bounds(4.into()),
|
||||
Some(RectF::new(vec2f(4., 16.), char_size))
|
||||
Some(RectF::new(vec2f(0., 12.), char_size))
|
||||
);
|
||||
|
||||
// The middle of the first line of the second paragraph.
|
||||
assert_eq!(
|
||||
model.character_bounds(9.into()),
|
||||
Some(RectF::new(vec2f(14., 38.), char_size))
|
||||
Some(RectF::new(vec2f(10., 26.), char_size))
|
||||
);
|
||||
|
||||
// The end of the first line of the second paragraph.
|
||||
assert_eq!(
|
||||
model.character_bounds(11.into()),
|
||||
Some(RectF::new(vec2f(34., 38.), char_size))
|
||||
Some(RectF::new(vec2f(30., 26.), char_size))
|
||||
);
|
||||
|
||||
// The middle of the second line of the second paragraph.
|
||||
assert_eq!(
|
||||
model.character_bounds(13.into()),
|
||||
Some(RectF::new(vec2f(14., 48.), char_size))
|
||||
Some(RectF::new(vec2f(10., 36.), char_size))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -336,6 +334,7 @@ fn test_non_empty_content_can_hide_final_trailing_newline() {
|
||||
model.viewport.width().as_f32(),
|
||||
)),
|
||||
code_block_type: Default::default(),
|
||||
pending_mermaid_asset: None,
|
||||
});
|
||||
model.set_content(content);
|
||||
|
||||
@@ -353,7 +352,7 @@ fn test_empty_content_keeps_final_trailing_newline_when_suppressed() {
|
||||
model.set_show_final_trailing_newline_when_non_empty(false);
|
||||
|
||||
assert_eq!(model.blocks(), 1);
|
||||
assert_eq!(model.height(), 32.0.into_pixels());
|
||||
assert_eq!(model.height(), 24.0.into_pixels());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -612,6 +611,7 @@ fn test_first_line_bounds() {
|
||||
model.viewport.width().as_f32(),
|
||||
)),
|
||||
code_block_type: Default::default(),
|
||||
pending_mermaid_asset: None,
|
||||
});
|
||||
model.set_content(content);
|
||||
|
||||
@@ -619,12 +619,12 @@ fn test_first_line_bounds() {
|
||||
let text_block = content
|
||||
.block_at_offset(CharOffset::zero())
|
||||
.expect("Block should exist");
|
||||
// Because the paragraph is soft-wrapped, it doesn't need centering, so the top offset is 4px.
|
||||
// Because the paragraph is soft-wrapped, it doesn't need centering.
|
||||
assert_eq!(
|
||||
text_block.first_line_bounds().expect("Bounds should exist"),
|
||||
RectF::new(vec2f(0., 4.), vec2f(104., 10.))
|
||||
RectF::new(vec2f(0., 0.), vec2f(100., 10.))
|
||||
);
|
||||
assert_eq!(text_block.item.height().as_f32(), 48.);
|
||||
assert_eq!(text_block.item.height().as_f32(), 40.);
|
||||
|
||||
let list_block = content
|
||||
.block_at_offset(CharOffset::from(33))
|
||||
@@ -632,7 +632,7 @@ fn test_first_line_bounds() {
|
||||
assert_eq!(
|
||||
list_block.first_line_bounds().expect("Bounds should exist"),
|
||||
RectF::new(
|
||||
vec2f(0., 52.),
|
||||
vec2f(0., 44.),
|
||||
vec2f(
|
||||
64., /* 4px margin + 20px list padding + 40px of text */
|
||||
10.
|
||||
@@ -649,7 +649,7 @@ fn test_first_line_bounds() {
|
||||
.first_line_bounds()
|
||||
.expect("Bounds should exist"),
|
||||
RectF::new(
|
||||
vec2f(0., 70. /* 66px y-offset + 4px margin */),
|
||||
vec2f(0., 62. /* 58px y-offset + 4px margin */),
|
||||
vec2f(
|
||||
144., /* 4px margin + 40px list padding + 10px of text - the test layout logic doesn't account for spacing */
|
||||
10.
|
||||
@@ -664,7 +664,7 @@ fn test_first_line_bounds() {
|
||||
assert_eq!(
|
||||
code_block.first_line_bounds().expect("Bounds should exist"),
|
||||
RectF::new(
|
||||
vec2f(0., 112. /* 104px y-offset + 8px margin */),
|
||||
vec2f(0., 104. /* 96px y-offset + 8px margin */),
|
||||
vec2f(
|
||||
70., /* 4px margin + 16px padding + 50px text */
|
||||
16. /* 16px padding area */
|
||||
@@ -684,10 +684,8 @@ fn test_first_line_bounds() {
|
||||
.first_line_bounds()
|
||||
.expect("Bounds should exist"),
|
||||
RectF::new(
|
||||
vec2f(
|
||||
0., 219., /* 198px y-offset + 14px margin + 7px centering */
|
||||
),
|
||||
vec2f(5. /* 4px margin + 1px cursor */, 10.)
|
||||
vec2f(0., 207. /* 200px y-offset + 7px centering */,),
|
||||
vec2f(1. /* 1px cursor */, 10.)
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -715,8 +713,8 @@ fn test_scroll_snapshot() {
|
||||
layout_content(&mut model);
|
||||
|
||||
let content = model.content();
|
||||
// Verify the height of each block. Each text paragraph has 8px of vertical padding and 10px
|
||||
// per soft-wrapped line. The trailing newline block is 32px high.
|
||||
// Verify the height of each block. Each text paragraph has 10px per soft-wrapped line with a
|
||||
// 24px minimum height. The trailing newline block is 24px high.
|
||||
assert_eq!(
|
||||
content
|
||||
.block_at_offset(CharOffset::zero())
|
||||
@@ -724,7 +722,7 @@ fn test_scroll_snapshot() {
|
||||
.item
|
||||
.height()
|
||||
.as_f32(),
|
||||
38.
|
||||
30.
|
||||
);
|
||||
assert_eq!(
|
||||
content
|
||||
@@ -733,7 +731,7 @@ fn test_scroll_snapshot() {
|
||||
.item
|
||||
.height()
|
||||
.as_f32(),
|
||||
48.
|
||||
40.
|
||||
);
|
||||
assert_eq!(
|
||||
content
|
||||
@@ -742,14 +740,14 @@ fn test_scroll_snapshot() {
|
||||
.item
|
||||
.height()
|
||||
.as_f32(),
|
||||
32.
|
||||
24.
|
||||
);
|
||||
drop(content);
|
||||
|
||||
// Scroll so that the EEEE line is at the top of the viewport.
|
||||
model.viewport.scroll((-52.).into_pixels(), model.height());
|
||||
model.viewport.scroll((-44.).into_pixels(), model.height());
|
||||
let scroll_position = model.snapshot_scroll_position();
|
||||
assert_eq!(scroll_position.first_character_offset(), 17.into());
|
||||
assert_eq!(scroll_position.first_character_offset(), 13.into());
|
||||
|
||||
// Now, double the viewport width, halving the number of soft-wrapped lines.
|
||||
model
|
||||
@@ -758,11 +756,11 @@ fn test_scroll_snapshot() {
|
||||
|
||||
// At first, the content will not have been laid out again, so the scroll position is
|
||||
// unaffected.
|
||||
assert_eq!(model.viewport.scroll_top(), 52.0.into_pixels());
|
||||
// After laying out again, each block is exactly 32px high (the two soft-wrapped blocks are
|
||||
assert_eq!(model.viewport.scroll_top(), 34.0.into_pixels());
|
||||
// After laying out again, each block is exactly 24px high (the two soft-wrapped blocks are
|
||||
// below the minimum height otherwise).
|
||||
layout_content(&mut model);
|
||||
assert_eq!(model.height().as_f32(), 32. * 3.);
|
||||
assert_eq!(model.height().as_f32(), 24. * 3.);
|
||||
|
||||
// Restore the scroll position at the new height. It should still start at the same content.
|
||||
assert!(
|
||||
@@ -770,16 +768,15 @@ fn test_scroll_snapshot() {
|
||||
.viewport
|
||||
.scroll_to(scroll_position.to_scroll_top(&model), model.height())
|
||||
);
|
||||
// The new scroll position is 32px (the first block) plus 4px of padding on the second block.
|
||||
// The EEEE line is now part of that first line.
|
||||
assert_eq!(model.viewport.scroll_top().as_f32(), 36.);
|
||||
// The reduced content height clamps the restored position to the last viewport.
|
||||
assert_eq!(model.viewport.scroll_top().as_f32(), 12.);
|
||||
|
||||
// Halve the original viewport width, leading to twice as many soft-wrapped lines.
|
||||
model
|
||||
.viewport
|
||||
.set_size(vec2f(20., 60.), model.width(), model.height());
|
||||
layout_content(&mut model);
|
||||
assert_eq!(model.height().as_f32(), 68. + 88. + 32.);
|
||||
assert_eq!(model.height().as_f32(), 60. + 80. + 24.);
|
||||
|
||||
// Restore the scroll position at the new height.
|
||||
assert!(
|
||||
@@ -787,8 +784,8 @@ fn test_scroll_snapshot() {
|
||||
.viewport
|
||||
.scroll_to(scroll_position.to_scroll_top(&model), model.height())
|
||||
);
|
||||
// The new scroll position is on the third soft-wrapped line of the second paragraph.
|
||||
assert_eq!(model.viewport.scroll_top().as_f32(), 92.);
|
||||
// The new scroll position is at the start of the second paragraph.
|
||||
assert_eq!(model.viewport.scroll_top().as_f32(), 60.);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1158,17 +1155,17 @@ fn make_test_cell_layout() -> CellLayout {
|
||||
line_char_ranges: vec![CharOffset::from(0)..CharOffset::from(3)],
|
||||
line_widths: vec![30.0],
|
||||
line_caret_positions: vec![vec![
|
||||
galaxyui::text_layout::CaretPosition {
|
||||
galaxyui_core::text_layout::CaretPosition {
|
||||
position_in_line: 0.0,
|
||||
start_offset: 0,
|
||||
last_offset: 0,
|
||||
},
|
||||
galaxyui::text_layout::CaretPosition {
|
||||
galaxyui_core::text_layout::CaretPosition {
|
||||
position_in_line: 10.0,
|
||||
start_offset: 1,
|
||||
last_offset: 1,
|
||||
},
|
||||
galaxyui::text_layout::CaretPosition {
|
||||
galaxyui_core::text_layout::CaretPosition {
|
||||
position_in_line: 20.0,
|
||||
start_offset: 2,
|
||||
last_offset: 2,
|
||||
@@ -1429,3 +1426,245 @@ fn test_link_at_offset_uses_cached_cell_links() {
|
||||
assert_eq!(table.link_at_offset(CharOffset::from(0)), None);
|
||||
assert_eq!(table.link_at_offset(CharOffset::from(3)), None);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// CharCell (TUI) layout helper tests
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
mod char_cell {
|
||||
|
||||
use crate::render::model::{
|
||||
ColumnUnit, LineCount, SoftWrapPoint, char_cell_display_width, char_cell_line_row_starts,
|
||||
char_cell_max_line, char_cell_offset_to_softwrap_point, char_cell_softwrap_point_to_offset,
|
||||
};
|
||||
|
||||
/// Build the `(line_starts, char_widths)` pair from a text string (mirrors
|
||||
/// `CharCellState::update_text` logic) so tests can construct the
|
||||
/// char-cell layout inputs without a full `RenderState`. `char_widths` holds
|
||||
/// the per-char display width (the derived data the layout actually needs).
|
||||
fn line_starts_for(text: &str) -> (Vec<usize>, Vec<u8>) {
|
||||
let char_widths: Vec<u8> = text
|
||||
.chars()
|
||||
.map(|c| char_cell_display_width(c) as u8)
|
||||
.collect();
|
||||
let mut starts = vec![0_usize];
|
||||
for (i, ch) in text.chars().enumerate() {
|
||||
if ch == '\n' {
|
||||
starts.push(i + 1);
|
||||
}
|
||||
}
|
||||
(starts, char_widths)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_line_empty() {
|
||||
let (starts, widths) = line_starts_for("");
|
||||
// Empty content → 1 visual row.
|
||||
assert_eq!(char_cell_max_line(&starts, &widths, 80), LineCount(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_line_single_short_line() {
|
||||
let (starts, widths) = line_starts_for("hello");
|
||||
assert_eq!(char_cell_max_line(&starts, &widths, 80), LineCount(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_line_single_wrapping_line() {
|
||||
// 10 chars, width 4 → ceil(10/4) = 3 rows.
|
||||
let (starts, widths) = line_starts_for("0123456789");
|
||||
assert_eq!(char_cell_max_line(&starts, &widths, 4), LineCount(3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_line_two_logical_lines() {
|
||||
// "abc\ndef": line0 = 3 chars (1 row at width 10), line1 = 3 chars (1 row) → 2.
|
||||
let (starts, widths) = line_starts_for("abc\ndef");
|
||||
assert_eq!(char_cell_max_line(&starts, &widths, 10), LineCount(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_line_empty_logical_line() {
|
||||
// "\n": two logical lines, both empty → 2 rows.
|
||||
let (starts, widths) = line_starts_for("\n");
|
||||
assert_eq!(char_cell_max_line(&starts, &widths, 80), LineCount(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offset_to_softwrap_single_line_short() {
|
||||
let text = "hello";
|
||||
let (starts, widths) = line_starts_for(text);
|
||||
// The softwrap API is 0-based, so char 'h' = index 0, 'e' = 1, ...
|
||||
// 'h' should be at (row=0, col=0).
|
||||
let pt = char_cell_offset_to_softwrap_point(CharOffset::from(0), &starts, &widths, 80);
|
||||
assert_eq!(pt, SoftWrapPoint::new(0, ColumnUnit::Chars(0)));
|
||||
// 'l' (3rd char, index 2) at col 2.
|
||||
let pt = char_cell_offset_to_softwrap_point(CharOffset::from(2), &starts, &widths, 80);
|
||||
assert_eq!(pt, SoftWrapPoint::new(0, ColumnUnit::Chars(2)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offset_to_softwrap_wrapping_line() {
|
||||
// width=4, "0123456789" — char index 4 should be on row 1, col 0.
|
||||
let text = "0123456789";
|
||||
let (starts, widths) = line_starts_for(text);
|
||||
// index 4 → row 4/4=1, col 0.
|
||||
let pt = char_cell_offset_to_softwrap_point(CharOffset::from(4), &starts, &widths, 4);
|
||||
assert_eq!(pt, SoftWrapPoint::new(1, ColumnUnit::Chars(0)));
|
||||
// index 7 → row 7/4=1, col 7%4=3.
|
||||
let pt = char_cell_offset_to_softwrap_point(CharOffset::from(7), &starts, &widths, 4);
|
||||
assert_eq!(pt, SoftWrapPoint::new(1, ColumnUnit::Chars(3)));
|
||||
// index 9 → row 9/4=2, col 9%4=1.
|
||||
let pt = char_cell_offset_to_softwrap_point(CharOffset::from(9), &starts, &widths, 4);
|
||||
assert_eq!(pt, SoftWrapPoint::new(2, ColumnUnit::Chars(1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offset_to_softwrap_two_logical_lines() {
|
||||
// "abc\ndef", width=10
|
||||
// 'a'=index0→(row0,col0), 'd'=index4→(row1,col0)
|
||||
let text = "abc\ndef";
|
||||
let (starts, widths) = line_starts_for(text);
|
||||
let pt_a = char_cell_offset_to_softwrap_point(CharOffset::from(0), &starts, &widths, 10);
|
||||
assert_eq!(pt_a, SoftWrapPoint::new(0, ColumnUnit::Chars(0)));
|
||||
// 'd' = index 4 (after 'abc\n'). Logical line 1, offset_in_line=0.
|
||||
let pt_d = char_cell_offset_to_softwrap_point(CharOffset::from(4), &starts, &widths, 10);
|
||||
assert_eq!(pt_d, SoftWrapPoint::new(1, ColumnUnit::Chars(0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn softwrap_roundtrip_single_line() {
|
||||
let text = "hello world";
|
||||
let (starts, widths) = line_starts_for(text);
|
||||
for i in 0..=(widths.len() as u64) {
|
||||
let offset = CharOffset::from(i as usize);
|
||||
let pt = char_cell_offset_to_softwrap_point(offset, &starts, &widths, 80);
|
||||
// Verify the column is ColumnUnit::Chars
|
||||
assert!(
|
||||
matches!(pt.column(), ColumnUnit::Chars(_)),
|
||||
"index {i}: expected Chars variant"
|
||||
);
|
||||
let back = char_cell_softwrap_point_to_offset(pt, &starts, &widths, 80);
|
||||
assert_eq!(back, offset, "round-trip failed at index {i}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn softwrap_roundtrip_wrapping() {
|
||||
let text = "abcdefghij"; // 10 chars
|
||||
let (starts, widths) = line_starts_for(text);
|
||||
for i in 0..10 {
|
||||
let offset = CharOffset::from(i);
|
||||
let pt = char_cell_offset_to_softwrap_point(offset, &starts, &widths, 4);
|
||||
let back = char_cell_softwrap_point_to_offset(pt, &starts, &widths, 4);
|
||||
assert_eq!(back, offset, "round-trip failed at index {i} with width=4");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn softwrap_point_to_offset_clamps_to_shorter_final_line() {
|
||||
// "abcd\nx": logical line 0 = "abcd" (4 chars), final line = "x" (1 char).
|
||||
// Moving down from column 3 of the first line targets (row 1, col 3),
|
||||
// but the final line only has 1 char — the result must clamp to the end
|
||||
// of the buffer (offset 6 = total chars), never past it.
|
||||
let text = "abcd\nx";
|
||||
let (starts, widths) = line_starts_for(text);
|
||||
assert_eq!(widths.len(), 6);
|
||||
let pt = SoftWrapPoint::new(1, ColumnUnit::Chars(3));
|
||||
let offset = char_cell_softwrap_point_to_offset(pt, &starts, &widths, 80);
|
||||
// Final line starts at char index 5 ("x"); clamped end is 5 + 1 = 6.
|
||||
assert_eq!(offset, CharOffset::from(6));
|
||||
assert!(
|
||||
offset <= CharOffset::from(widths.len()),
|
||||
"offset {offset:?} must not exceed total chars {}",
|
||||
widths.len()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn softwrap_returns_chars_variant_not_pixels() {
|
||||
let text = "abc";
|
||||
let (starts, widths) = line_starts_for(text);
|
||||
let pt = char_cell_offset_to_softwrap_point(CharOffset::from(0), &starts, &widths, 80);
|
||||
assert!(
|
||||
matches!(pt.column(), ColumnUnit::Chars(_)),
|
||||
"CharCell path must return ColumnUnit::Chars, got {:?}",
|
||||
pt.column()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn softwrap_point_zero_offset_is_row0_col0() {
|
||||
let text = "abc";
|
||||
let (starts, widths) = line_starts_for(text);
|
||||
// Index 0 = first char → (0, 0).
|
||||
let pt = char_cell_offset_to_softwrap_point(CharOffset::from(0), &starts, &widths, 80);
|
||||
assert_eq!(pt.row(), 0);
|
||||
assert_eq!(pt.column(), ColumnUnit::Chars(0));
|
||||
}
|
||||
|
||||
// ── Unicode display width ───────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn display_width_basic() {
|
||||
assert_eq!(char_cell_display_width('a'), 1);
|
||||
// CJK ideographs are double-width.
|
||||
assert_eq!(char_cell_display_width('你'), 2);
|
||||
// A combining acute accent is zero-width.
|
||||
assert_eq!(char_cell_display_width('\u{0301}'), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wide_char_occupies_two_columns() {
|
||||
// "你好world": 你(2) 好(2) w o r l d. Index 2 ('w') sits at display col 4.
|
||||
let text = "你好world";
|
||||
let (starts, widths) = line_starts_for(text);
|
||||
let pt = char_cell_offset_to_softwrap_point(CharOffset::from(2), &starts, &widths, 80);
|
||||
assert_eq!(pt, SoftWrapPoint::new(0, ColumnUnit::Chars(4)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wide_char_wraps_when_it_does_not_fit() {
|
||||
// "你好你" at width 4: 你好 fill the first row (4 cols); the third 你
|
||||
// doesn't fit so it wraps to row 1.
|
||||
let text = "你好你";
|
||||
let (starts, widths) = line_starts_for(text);
|
||||
assert_eq!(char_cell_max_line(&starts, &widths, 4), LineCount(2));
|
||||
// Cursor before the third 你 (index 2) is at the start of row 1.
|
||||
let pt = char_cell_offset_to_softwrap_point(CharOffset::from(2), &starts, &widths, 4);
|
||||
assert_eq!(pt, SoftWrapPoint::new(1, ColumnUnit::Chars(0)));
|
||||
// Round-trips at each char boundary.
|
||||
for i in 0..=widths.len() {
|
||||
let offset = CharOffset::from(i);
|
||||
let pt = char_cell_offset_to_softwrap_point(offset, &starts, &widths, 4);
|
||||
let back = char_cell_softwrap_point_to_offset(pt, &starts, &widths, 4);
|
||||
assert_eq!(back, offset, "wide-char round-trip failed at index {i}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_width_char_does_not_advance_column() {
|
||||
// "a\u{0301}b": 'a' + combining acute (0 width) + 'b'. The combining
|
||||
// mark shares 'a's column, so 'b' sits at col 1 (not 2).
|
||||
let text = "a\u{0301}b";
|
||||
let (starts, widths) = line_starts_for(text);
|
||||
// Gap before 'b' (index 2) shares the accent's column.
|
||||
let pt = char_cell_offset_to_softwrap_point(CharOffset::from(2), &starts, &widths, 80);
|
||||
assert_eq!(pt, SoftWrapPoint::new(0, ColumnUnit::Chars(1)));
|
||||
// End of line (index 3, after 'b') is at col 2.
|
||||
let pt = char_cell_offset_to_softwrap_point(CharOffset::from(3), &starts, &widths, 80);
|
||||
assert_eq!(pt, SoftWrapPoint::new(0, ColumnUnit::Chars(2)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_row_starts_breaks_on_wide_chars() {
|
||||
// width 4, "你好你好": two wide chars per row → break before index 2.
|
||||
let widths: Vec<u8> = "你好你好"
|
||||
.chars()
|
||||
.map(|c| char_cell_display_width(c) as u8)
|
||||
.collect();
|
||||
assert_eq!(char_cell_line_row_starts(&widths, 4), vec![0, 2]);
|
||||
// width 0 disables wrapping.
|
||||
assert_eq!(char_cell_line_row_starts(&widths, 0), vec![0]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ use std::ops::{Add, Sub};
|
||||
|
||||
use itertools::Itertools;
|
||||
use num_traits::SaturatingSub;
|
||||
|
||||
use string_offset::CharOffset;
|
||||
|
||||
use super::FrameOffset;
|
||||
@@ -19,7 +18,7 @@ mod tests;
|
||||
/// certain character runs are interactive. Within those runs, there's a 1:1 mapping between
|
||||
/// `char`s in the content model and `char`s in the text frame. It translates in two directions:
|
||||
/// * From a [`CharOffset`] relative to the start of the content model block (a [`super::Paragraph`])
|
||||
/// to the character index in the [`galaxyui::text_layout::TextFrame`].
|
||||
/// to the character index in the [`galaxyui_core::text_layout::TextFrame`].
|
||||
/// * From a [`FrameOffset`] in the `TextFrame` to the closest `CharOffset` in the content model
|
||||
/// block (for example, clicking within a placeholder should snap the cursor to a regular content
|
||||
/// character).
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use string_offset::CharOffset;
|
||||
|
||||
use super::{OffsetMap, SelectableTextRun};
|
||||
use crate::render::model::FrameOffset;
|
||||
use string_offset::CharOffset;
|
||||
|
||||
#[test]
|
||||
fn test_offset_map_basic() {
|
||||
@@ -76,24 +77,20 @@ fn test_offset_map_placeholders() {
|
||||
fn test_end_to_end() {
|
||||
// Group imports here so they don't cause "unused import" warnings on other targets.
|
||||
|
||||
use galaxyui::{
|
||||
App, color::ColorU, elements::Fill, fonts::Cache as FontCache, text_layout::LayoutCache,
|
||||
};
|
||||
use galaxyui_core::App;
|
||||
use galaxyui_core::color::ColorU;
|
||||
use galaxyui_core::elements::Fill;
|
||||
use galaxyui_core::fonts::Cache as FontCache;
|
||||
use galaxyui_core::text_layout::LayoutCache;
|
||||
|
||||
use crate::{
|
||||
content::{
|
||||
buffer::{Buffer, BufferEditAction, EditOrigin},
|
||||
selection_model::BufferSelectionModel,
|
||||
text::IndentBehavior,
|
||||
},
|
||||
render::{
|
||||
layout::TextLayout,
|
||||
model::{
|
||||
BlockItem, BrokenLinkStyle, CheckBoxStyle, HorizontalRuleStyle, InlineCodeStyle,
|
||||
PARAGRAPH_MIN_HEIGHT, ParagraphStyles, RenderLayoutOptions, RichTextStyles,
|
||||
TableStyle, test_utils::TEST_BASELINE_OFFSET,
|
||||
},
|
||||
},
|
||||
use crate::content::buffer::{Buffer, BufferEditAction, EditOrigin};
|
||||
use crate::content::selection_model::BufferSelectionModel;
|
||||
use crate::content::text::IndentBehavior;
|
||||
use crate::render::layout::TextLayout;
|
||||
use crate::render::model::test_utils::TEST_BASELINE_OFFSET;
|
||||
use crate::render::model::{
|
||||
BlockItem, BrokenLinkStyle, CheckBoxStyle, HorizontalRuleStyle, InlineCodeStyle,
|
||||
PARAGRAPH_MIN_HEIGHT, ParagraphStyles, RenderLayoutOptions, RichTextStyles, TableStyle,
|
||||
};
|
||||
|
||||
App::test((), |mut app| async move {
|
||||
@@ -214,7 +211,7 @@ fn test_end_to_end() {
|
||||
delta.layout_delta(
|
||||
&text_layout,
|
||||
None,
|
||||
RenderLayoutOptions::default(),
|
||||
&RenderLayoutOptions::default(),
|
||||
None,
|
||||
ctx,
|
||||
)
|
||||
|
||||
@@ -3,20 +3,17 @@
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use galaxyui::{
|
||||
geometry::vector::Vector2F,
|
||||
text_layout::Line,
|
||||
units::{IntoPixels, Pixels},
|
||||
};
|
||||
use sum_tree::{Cursor, Dimension};
|
||||
|
||||
use crate::render::layout::line_height;
|
||||
use string_offset::CharOffset;
|
||||
use sum_tree::{Cursor, Dimension};
|
||||
use galaxyui_core::geometry::vector::Vector2F;
|
||||
use galaxyui_core::text_layout::Line;
|
||||
use galaxyui_core::units::{IntoPixels, Pixels};
|
||||
|
||||
use super::{
|
||||
BlockItem, BlockSpacing, HorizontalRuleConfig, ImageBlockConfig, LaidOutEmbeddedItem,
|
||||
LaidOutTable, LayoutSummary, LineCount, Paragraph, ParagraphBlock, RenderContext, bounds,
|
||||
};
|
||||
use crate::render::layout::line_height;
|
||||
|
||||
/// Wrapper to track an item's position, both in the buffer and on the screen.
|
||||
#[derive(Debug)]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use galaxyui::EntityId;
|
||||
use galaxyui_core::EntityId;
|
||||
|
||||
/// Utility for consistently creating and referencing saved position IDs for
|
||||
/// rich text.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::*;
|
||||
use markdown_parser::Hyperlink;
|
||||
use markdown_parser::parse_inline_markdown;
|
||||
use markdown_parser::weight::CustomWeight;
|
||||
use markdown_parser::{Hyperlink, parse_inline_markdown};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_simple_table() {
|
||||
|
||||
@@ -1,26 +1,24 @@
|
||||
//! Test helpers for all render model tests.
|
||||
|
||||
use parking_lot::Once;
|
||||
use std::{mem, sync::Arc};
|
||||
use vec1::{Vec1, vec1};
|
||||
use std::mem;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::content::text::BufferBlockStyle;
|
||||
use galaxyui::elements::ListIndentLevel;
|
||||
use galaxyui::{
|
||||
color::ColorU,
|
||||
elements::{Border, Fill},
|
||||
fonts::{FamilyId, Weight},
|
||||
geometry::vector::vec2f,
|
||||
text_layout::{CaretPosition, Glyph, Line, Run, TextFrame},
|
||||
units::{IntoPixels, Pixels},
|
||||
};
|
||||
use ordered_float::OrderedFloat;
|
||||
use parking_lot::Once;
|
||||
use vec1::{Vec1, vec1};
|
||||
use galaxyui_core::color::ColorU;
|
||||
use galaxyui_core::elements::{Border, Fill, ListIndentLevel};
|
||||
use galaxyui_core::fonts::{FamilyId, Weight};
|
||||
use galaxyui_core::geometry::vector::vec2f;
|
||||
use galaxyui_core::text_layout::{CaretPosition, Glyph, Line, Run, TextFrame};
|
||||
use galaxyui_core::units::{IntoPixels, Pixels};
|
||||
|
||||
use super::{
|
||||
BlockItem, BrokenLinkStyle, CheckBoxStyle, DEFAULT_BLOCK_SPACINGS, HorizontalRuleStyle,
|
||||
InlineCodeStyle, OffsetMap, PARAGRAPH_MIN_HEIGHT, Paragraph, ParagraphStyles, RichTextStyles,
|
||||
TEXT_SPACING, TableStyle,
|
||||
};
|
||||
use crate::content::text::BufferBlockStyle;
|
||||
|
||||
pub const TEST_BASELINE_OFFSET: f32 = 0.7;
|
||||
|
||||
@@ -241,7 +239,7 @@ pub fn layout(text: &str, styles: &RichTextStyles, max_width: impl IntoPixels) -
|
||||
width: line_width.as_f32(),
|
||||
trailing_whitespace_width: 0.,
|
||||
runs: vec![Run {
|
||||
font_id: galaxyui::fonts::FontId(0),
|
||||
font_id: galaxyui_core::fonts::FontId(0),
|
||||
styles: Default::default(),
|
||||
glyphs: mem::take(&mut glyphs_acc),
|
||||
width: line_width.as_f32(),
|
||||
@@ -280,7 +278,7 @@ pub fn layout(text: &str, styles: &RichTextStyles, max_width: impl IntoPixels) -
|
||||
width: line_width.as_f32(),
|
||||
trailing_whitespace_width: 0.,
|
||||
runs: vec![Run {
|
||||
font_id: galaxyui::fonts::FontId(0),
|
||||
font_id: galaxyui_core::fonts::FontId(0),
|
||||
styles: Default::default(),
|
||||
glyphs: glyphs_acc,
|
||||
width: line_width.as_f32(),
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
use float_cmp::ApproxEq;
|
||||
use galaxyui::{
|
||||
SizeConstraint,
|
||||
geometry::{
|
||||
rect::RectF,
|
||||
vector::{Vector2F, vec2f},
|
||||
},
|
||||
units::{IntoPixels, Pixels},
|
||||
};
|
||||
use sum_tree::{SeekBias, SumTree};
|
||||
|
||||
use crate::render::element::RenderContext;
|
||||
use string_offset::CharOffset;
|
||||
use sum_tree::{SeekBias, SumTree};
|
||||
use galaxyui_core::SizeConstraint;
|
||||
use galaxyui_core::geometry::rect::RectF;
|
||||
use galaxyui_core::geometry::vector::{Vector2F, vec2f};
|
||||
use galaxyui_core::units::{IntoPixels, Pixels};
|
||||
|
||||
use super::positioned::PositionedCursor;
|
||||
use super::{
|
||||
AUTO_SCROLL_MARGIN, BlockItem, BlockSpacing, Height, HitTestOptions, LayoutSummary, Location,
|
||||
RenderState, UNIT_MARGIN, bounds, positioned::PositionedCursor,
|
||||
RenderState, UNIT_MARGIN, bounds,
|
||||
};
|
||||
use crate::render::element::RenderContext;
|
||||
|
||||
/// For horizontal autoscrolling, it is very easy to "stuck" on a character if it is aligned exactly on the viewport boundary.
|
||||
/// To help make scrolling more smooth, add a small margin here to overcome these boundaries.
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
use itertools::Itertools;
|
||||
use sum_tree::SumTree;
|
||||
|
||||
use galaxyui::{
|
||||
SizeConstraint,
|
||||
geometry::vector::vec2f,
|
||||
units::{IntoPixels, Pixels},
|
||||
};
|
||||
|
||||
use crate::render::model::{
|
||||
RenderState,
|
||||
test_utils::{TEST_STYLES, mock_paragraph},
|
||||
};
|
||||
use galaxyui_core::SizeConstraint;
|
||||
use galaxyui_core::geometry::vector::vec2f;
|
||||
use galaxyui_core::units::{IntoPixels, Pixels};
|
||||
|
||||
use super::ViewportState;
|
||||
use crate::render::model::RenderState;
|
||||
use crate::render::model::test_utils::{TEST_STYLES, mock_paragraph};
|
||||
|
||||
#[test]
|
||||
fn test_viewport_offsets() {
|
||||
@@ -26,7 +20,7 @@ fn test_viewport_offsets() {
|
||||
content.push(mock_paragraph(80., 100., 1));
|
||||
render_state.set_content(content);
|
||||
|
||||
// Double-check the heights with margins+padding, as later tests rely on them.
|
||||
// Double-check the heights, as later tests rely on them.
|
||||
let heights = render_state
|
||||
.content
|
||||
.borrow()
|
||||
@@ -34,7 +28,7 @@ fn test_viewport_offsets() {
|
||||
.iter()
|
||||
.map(|item| item.height().as_f32())
|
||||
.collect_vec();
|
||||
assert_eq!(heights, vec![32., 68., 108., 38., 88., 32.]);
|
||||
assert_eq!(heights, vec![24., 60., 100., 30., 80., 24.]);
|
||||
|
||||
let content = render_state.content();
|
||||
let offsets = content
|
||||
@@ -53,12 +47,14 @@ fn test_viewport_offsets() {
|
||||
vec![
|
||||
// The first item is fully above the viewport.
|
||||
// The second item is slightly above the viewport.
|
||||
(-8., 1),
|
||||
(-16., 1),
|
||||
// The third is fully within the viewport
|
||||
(60., 2),
|
||||
(44., 2),
|
||||
// The fourth is slightly past the viewport, and cut off.
|
||||
(168., 4)
|
||||
] // The fifth item is fully after the viewport.
|
||||
(144., 4),
|
||||
// The fifth is slightly past the viewport, and cut off.
|
||||
(174., 7)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+15
-12
@@ -5,17 +5,15 @@ use galaxyui::{Entity, ModelContext, ModelHandle, r#async::SpawnedFutureHandle};
|
||||
use itertools::Itertools;
|
||||
use lazy_static::lazy_static;
|
||||
use pathfinder_color::ColorU;
|
||||
|
||||
use crate::{
|
||||
content::{
|
||||
anchor::Anchor,
|
||||
buffer::{Buffer, BufferEvent},
|
||||
find::{Query, SearchConfig, SearchResults},
|
||||
selection_model::BufferSelectionModel,
|
||||
},
|
||||
render::model::Decoration,
|
||||
};
|
||||
use string_offset::CharOffset;
|
||||
use galaxyui_core::r#async::SpawnedFutureHandle;
|
||||
use galaxyui_core::{Entity, ModelContext, ModelHandle};
|
||||
|
||||
use crate::content::anchor::Anchor;
|
||||
use crate::content::buffer::{Buffer, BufferEvent};
|
||||
use crate::content::find::{Query, SearchConfig, SearchResults};
|
||||
use crate::content::selection_model::BufferSelectionModel;
|
||||
use crate::render::model::Decoration;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "search_tests.rs"]
|
||||
@@ -267,7 +265,12 @@ impl Searcher {
|
||||
self.buffer.as_ref(ctx).prepare_search(&config)
|
||||
}
|
||||
|
||||
fn handle_buffer_event(&mut self, event: &BufferEvent, ctx: &mut ModelContext<Self>) {
|
||||
fn handle_buffer_event(
|
||||
&mut self,
|
||||
_: ModelHandle<Buffer>,
|
||||
event: &BufferEvent,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if let BufferEvent::ContentChanged { .. } = event {
|
||||
self.run_search(ctx);
|
||||
}
|
||||
@@ -276,7 +279,7 @@ impl Searcher {
|
||||
#[cfg(test)]
|
||||
pub fn search_finished(
|
||||
&self,
|
||||
ctx: &mut galaxyui::AppContext,
|
||||
ctx: &mut galaxyui_core::AppContext,
|
||||
) -> impl std::future::Future<Output = ()> + use<> {
|
||||
let maybe_search = self
|
||||
.search_handle
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
use galaxyui::{App, ModelHandle};
|
||||
|
||||
use crate::content::{
|
||||
buffer::{AutoScrollBehavior, Buffer, BufferEditAction, BufferSelectAction, EditOrigin},
|
||||
find::{Match, SearchResults},
|
||||
selection_model::BufferSelectionModel,
|
||||
text::IndentBehavior,
|
||||
};
|
||||
use galaxyui_core::{App, ModelHandle};
|
||||
|
||||
use super::Searcher;
|
||||
use crate::content::buffer::{
|
||||
AutoScrollBehavior, Buffer, BufferEditAction, BufferSelectAction, EditOrigin,
|
||||
};
|
||||
use crate::content::find::{Match, SearchResults};
|
||||
use crate::content::selection_model::BufferSelectionModel;
|
||||
use crate::content::text::IndentBehavior;
|
||||
|
||||
#[test]
|
||||
fn test_literal_search() {
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
use galaxyui::{AppContext, Entity, ModelAsRef, ModelContext, ModelHandle, units::Pixels};
|
||||
use num_traits::SaturatingSub;
|
||||
use std::ops::Range;
|
||||
use vec1::Vec1;
|
||||
|
||||
use crate::{
|
||||
content::{
|
||||
buffer::{
|
||||
AutoScrollBehavior, Buffer, BufferEvent, BufferSelectAction, SelectionOffsets,
|
||||
ToBufferCharOffset, ToBufferPoint,
|
||||
},
|
||||
hidden_lines_model::HiddenLinesModel,
|
||||
selection_model::BufferSelectionModel,
|
||||
text::{BlockType, BufferBlockStyle, CodeBlockType},
|
||||
},
|
||||
render::model::{RenderState, SoftWrapPoint},
|
||||
};
|
||||
use galaxyui::text::{TextBuffer, point::Point, word_boundaries::WordBoundariesPolicy};
|
||||
use num_traits::SaturatingSub;
|
||||
use string_offset::CharOffset;
|
||||
use vec1::Vec1;
|
||||
use galaxyui_core::text::TextBuffer;
|
||||
use galaxyui_core::text::point::Point;
|
||||
use galaxyui_core::text::word_boundaries::WordBoundariesPolicy;
|
||||
use galaxyui_core::{AppContext, Entity, ModelAsRef, ModelContext, ModelHandle};
|
||||
|
||||
use crate::content::buffer::{
|
||||
AutoScrollBehavior, Buffer, BufferEvent, BufferSelectAction, SelectionOffsets,
|
||||
ToBufferCharOffset, ToBufferPoint,
|
||||
};
|
||||
use crate::content::hidden_lines_model::HiddenLinesModel;
|
||||
use crate::content::selection_model::BufferSelectionModel;
|
||||
use crate::content::text::{BlockType, BufferBlockStyle, CodeBlockType};
|
||||
use crate::render::model::{ColumnUnit, RenderState, SoftWrapPoint};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "selection_tests.rs"]
|
||||
@@ -29,13 +28,13 @@ pub struct SelectionModel {
|
||||
selection_model: ModelHandle<BufferSelectionModel>,
|
||||
hidden_lines: Option<ModelHandle<HiddenLinesModel>>,
|
||||
|
||||
/// The goal x-coordinate in pixels. When moving between lines, the desired column
|
||||
/// might not exist on the new line (because it's shorter than the previous line). Storing
|
||||
/// the goal column lets us move back to that column when changing to a longer line.
|
||||
/// The goal column when moving between lines. The desired column might not exist on the
|
||||
/// new line (because it's shorter than the previous line). Storing the goal column lets
|
||||
/// us move back to that column when changing to a longer line.
|
||||
///
|
||||
/// This is stored in pixels instead of characters so that, like other editors, we can
|
||||
/// match the visual column, accounting for any differences in padding and character width.
|
||||
pub goal_xs: Option<Vec1<Pixels>>,
|
||||
/// Uses [`ColumnUnit`] to work in either pixel coordinates (GUI path) or char-cell
|
||||
/// coordinates (TUI path). All values must use the same variant within one session.
|
||||
pub goal_xs: Option<Vec1<ColumnUnit>>,
|
||||
|
||||
/// The in-progress selection.
|
||||
pending_selection: Option<PendingSelection>,
|
||||
@@ -95,7 +94,7 @@ pub struct NavigationResult {
|
||||
/// The resulting character offset from text navigation.
|
||||
pub offset: CharOffset,
|
||||
/// The goal column based on the original offset, if it's different from the character offset.
|
||||
pub goal_x: Option<Pixels>,
|
||||
pub goal_x: Option<ColumnUnit>,
|
||||
}
|
||||
|
||||
impl SelectionModel {
|
||||
@@ -664,19 +663,19 @@ impl SelectionModel {
|
||||
}
|
||||
|
||||
/// Modify all of the selections in a given way.
|
||||
/// The currently active selections are looped though, along with any current x-pixel goal values,
|
||||
/// and a new head and tail offsets and a new x-pixel goal value are calculated for each selection.
|
||||
/// The currently active selections are looped though, along with any current goal column values,
|
||||
/// and a new head, tail, and goal column are calculated for each selection.
|
||||
///
|
||||
/// selection_update: A function that takes the current selection, the current goal x, and the current
|
||||
/// selection offsets, and returns the new goal x and the new selection offsets.
|
||||
/// selection_update: A function that takes the current selection, the current goal column, and
|
||||
/// the current selection offsets, and returns the new goal column and new selection offsets.
|
||||
fn update_selections_internal<T>(&mut self, selection_update: T, ctx: &mut ModelContext<Self>)
|
||||
where
|
||||
T: Fn(
|
||||
&SelectionModel,
|
||||
&mut ModelContext<SelectionModel>,
|
||||
&SelectionOffsets,
|
||||
&Option<Pixels>,
|
||||
) -> (Option<Pixels>, SelectionOffsets),
|
||||
&Option<ColumnUnit>,
|
||||
) -> (Option<ColumnUnit>, SelectionOffsets),
|
||||
{
|
||||
// Before we take action, merge any overlapping selections.
|
||||
self.selection_model.update(ctx, |selection_model, _ctx| {
|
||||
@@ -803,7 +802,7 @@ impl SelectionModel {
|
||||
direction: TextDirection,
|
||||
unit: TextUnit,
|
||||
step_size: u32,
|
||||
goal_x: Option<Pixels>,
|
||||
goal_x: Option<ColumnUnit>,
|
||||
ctx: &impl ModelAsRef,
|
||||
) -> NavigationResult {
|
||||
match unit {
|
||||
@@ -870,7 +869,7 @@ impl SelectionModel {
|
||||
start: CharOffset,
|
||||
direction: TextDirection,
|
||||
step_size: u32,
|
||||
goal_x: Option<Pixels>,
|
||||
goal_x: Option<ColumnUnit>,
|
||||
ctx: &impl ModelAsRef,
|
||||
) -> NavigationResult {
|
||||
let render = self.render.as_ref(ctx);
|
||||
@@ -910,7 +909,7 @@ impl SelectionModel {
|
||||
// equivalent to self.goal_x.unwrap_or(next_point.column()), but captures the intent that we
|
||||
// want to stick to the rightmost point along the path, especially with proportional fonts.
|
||||
let goal_column = match goal_x {
|
||||
Some(x) => x.max(next_point.column()),
|
||||
Some(x) => x.col_max(next_point.column()),
|
||||
None => next_point.column(),
|
||||
};
|
||||
let goal_point = SoftWrapPoint::new(next_point.row(), goal_column);
|
||||
@@ -943,7 +942,7 @@ impl SelectionModel {
|
||||
let start_point = render.offset_to_softwrap_point(start.saturating_sub(&1.into()));
|
||||
let end_offset = match direction {
|
||||
TextDirection::Backwards => {
|
||||
let row_start = SoftWrapPoint::new(start_point.row(), Pixels::zero());
|
||||
let row_start = SoftWrapPoint::new(start_point.row(), ColumnUnit::pixels_zero());
|
||||
let soft_wrapped_start = render.softwrap_point_to_offset(row_start);
|
||||
|
||||
match content.indented_line_start(start) {
|
||||
@@ -968,8 +967,10 @@ impl SelectionModel {
|
||||
match content.indented_line_start(start) {
|
||||
Some(indented_start) if indented_start > start => indented_start,
|
||||
_ => {
|
||||
let next_row_start =
|
||||
SoftWrapPoint::new(start_point.row() + 1, Pixels::zero());
|
||||
let next_row_start = SoftWrapPoint::new(
|
||||
start_point.row() + 1,
|
||||
ColumnUnit::pixels_zero(),
|
||||
);
|
||||
// TODO(CLD-558): This should have a -1.
|
||||
render.softwrap_point_to_offset(next_row_start)
|
||||
}
|
||||
@@ -1004,7 +1005,12 @@ impl SelectionModel {
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_buffer_event(&mut self, event: &BufferEvent, ctx: &mut ModelContext<Self>) {
|
||||
fn handle_buffer_event(
|
||||
&mut self,
|
||||
_: ModelHandle<Buffer>,
|
||||
event: &BufferEvent,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
BufferEvent::ContentChanged { origin, .. } if origin.from_user() => self.goal_xs = None,
|
||||
BufferEvent::AnchorUpdated {
|
||||
@@ -1028,7 +1034,7 @@ impl NavigationResult {
|
||||
/// Creates a `NavigationResult` with both a new offset and an updated goal column.
|
||||
/// If the goal column does not exist on the new line, it may not correspond to the
|
||||
/// actual offset.
|
||||
pub fn for_offset_and_goal(offset: CharOffset, goal_x: Option<Pixels>) -> Self {
|
||||
pub fn for_offset_and_goal(offset: CharOffset, goal_x: Option<ColumnUnit>) -> Self {
|
||||
Self { offset, goal_x }
|
||||
}
|
||||
|
||||
|
||||
@@ -3,29 +3,25 @@ use std::sync::Arc;
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use galaxyui::{App, ModelAsRef, units::IntoPixels};
|
||||
use serde_yaml::Value;
|
||||
use string_offset::CharOffset;
|
||||
use sum_tree::SumTree;
|
||||
use vec1::vec1;
|
||||
|
||||
use crate::{
|
||||
content::{
|
||||
buffer::{
|
||||
AutoScrollBehavior, Buffer, BufferEditAction, BufferSelectAction, EditOrigin,
|
||||
InitialBufferState, SelectionOffsets, tests::TestEmbeddedItem,
|
||||
},
|
||||
selection_model::BufferSelectionModel,
|
||||
text::{BufferBlockStyle, IndentBehavior, IndentUnit},
|
||||
},
|
||||
render::model::{
|
||||
BlockItem, COMMAND_SPACING, ImageBlockConfig, RenderState,
|
||||
test_utils::{TEST_STYLES, laid_out_paragraph},
|
||||
},
|
||||
selection::SelectionMode,
|
||||
};
|
||||
use galaxyui::assets::asset_cache::AssetSource;
|
||||
use galaxyui::text::word_boundaries::WordBoundariesPolicy;
|
||||
use string_offset::CharOffset;
|
||||
use galaxyui_core::assets::asset_cache::AssetSource;
|
||||
use galaxyui_core::text::word_boundaries::WordBoundariesPolicy;
|
||||
use galaxyui_core::units::IntoPixels;
|
||||
use galaxyui_core::{App, ModelAsRef};
|
||||
|
||||
use super::{SelectionModel, TextDirection, TextUnit};
|
||||
use crate::content::buffer::tests::TestEmbeddedItem;
|
||||
use crate::content::buffer::{
|
||||
AutoScrollBehavior, Buffer, BufferEditAction, BufferSelectAction, EditOrigin,
|
||||
InitialBufferState, SelectionOffsets,
|
||||
};
|
||||
use crate::content::selection_model::BufferSelectionModel;
|
||||
use crate::content::text::{BufferBlockStyle, IndentBehavior, IndentUnit};
|
||||
use crate::render::model::test_utils::{TEST_STYLES, laid_out_paragraph};
|
||||
use crate::render::model::{BlockItem, COMMAND_SPACING, ColumnUnit, ImageBlockConfig, RenderState};
|
||||
use crate::selection::SelectionMode;
|
||||
|
||||
impl SelectionModel {
|
||||
/// The cursor location.
|
||||
@@ -37,7 +33,9 @@ impl SelectionModel {
|
||||
}
|
||||
}
|
||||
|
||||
fn selection_model_with_rendered_mermaid(app: &mut App) -> galaxyui::ModelHandle<SelectionModel> {
|
||||
fn selection_model_with_rendered_mermaid(
|
||||
app: &mut App,
|
||||
) -> galaxyui_core::ModelHandle<SelectionModel> {
|
||||
app.add_model(|ctx| {
|
||||
let buffer = ctx.add_model(|_| Buffer::new(Box::new(|_, _| IndentBehavior::Ignore)));
|
||||
let buffer_selection = ctx.add_model(|_| BufferSelectionModel::new(buffer.clone()));
|
||||
@@ -243,14 +241,14 @@ fn test_horizontal_movement_resets_goal_column() {
|
||||
|
||||
// Moving via the high-level navigation APIs should reset the goal column.
|
||||
selection.update(&mut app, |selection, ctx| {
|
||||
selection.goal_xs = Some(vec1::vec1![12.34.into_pixels()]);
|
||||
selection.goal_xs = Some(vec1::vec1![ColumnUnit::Pixels(12.34.into_pixels())]);
|
||||
selection.move_selection(TextDirection::Forwards, TextUnit::LineBoundary, ctx);
|
||||
assert_eq!(selection.goal_xs, None);
|
||||
});
|
||||
|
||||
// Moving via a buffer-level action should as well (as long as it's via the selection model).
|
||||
selection.update(&mut app, |selection, ctx| {
|
||||
selection.goal_xs = Some(vec1::vec1![12.34.into_pixels()]);
|
||||
selection.goal_xs = Some(vec1::vec1![ColumnUnit::Pixels(12.34.into_pixels())]);
|
||||
selection.update_selection(
|
||||
BufferSelectAction::MoveLeft,
|
||||
AutoScrollBehavior::Selection,
|
||||
@@ -261,7 +259,7 @@ fn test_horizontal_movement_resets_goal_column() {
|
||||
|
||||
// Editing resets the goal too.
|
||||
selection.update(&mut app, |selection, ctx| {
|
||||
selection.goal_xs = Some(vec1::vec1![12.34.into_pixels()]);
|
||||
selection.goal_xs = Some(vec1::vec1![ColumnUnit::Pixels(12.34.into_pixels())]);
|
||||
selection.content.update(ctx, |buffer, ctx| {
|
||||
buffer.update_content(
|
||||
BufferEditAction::Insert {
|
||||
|
||||
@@ -5743,12 +5743,9 @@ impl<T: core::error::Error> core::error::Error for Box<T> {
|
||||
|
||||
#![stable(feature = "rust1", since = "1.0.0")]
|
||||
|
||||
use core::error::Error;
|
||||
use core::fmt;
|
||||
use core::hash;
|
||||
#[cfg(not(no_global_oom_handling))]
|
||||
use core::iter::from_fn;
|
||||
use core::iter::FusedIterator;
|
||||
#[cfg(not(no_global_oom_handling))]
|
||||
use core::ops::Add;
|
||||
#[cfg(not(no_global_oom_handling))]
|
||||
@@ -5767,7 +5764,6 @@ use crate::collections::TryReserveError;
|
||||
use crate::str::{self, from_utf8_unchecked_mut, Chars, Utf8Error};
|
||||
#[cfg(not(no_global_oom_handling))]
|
||||
use crate::str::{from_boxed_utf8_unchecked, FromStr};
|
||||
use crate::vec::Vec;
|
||||
|
||||
/// A UTF-8–encoded, growable string.
|
||||
///
|
||||
|
||||
@@ -14,3 +14,9 @@ The `images/` directory contains sample images and a test markdown file (`image_
|
||||
- Empty alt text
|
||||
|
||||
To test image rendering, open `images/image_test.md` in Warp.
|
||||
|
||||
## ToC Navigation
|
||||
|
||||
`toc_anchor_test.md` covers manual validation for Markdown fragment link navigation (case-insensitive heading matching and scrolling).
|
||||
|
||||
To test, open `toc_anchor_test.md` in Warp's Markdown viewer and click the table-of-contents links.
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# Markdown ToC Manual Test
|
||||
|
||||
Use this file in a Warp notebook to verify fragment link navigation.
|
||||
|
||||
## Table of contents
|
||||
|
||||
- [Basic heading](#Basic heading)
|
||||
- [Mixed Case](#Mixed CASE Heading)
|
||||
- [Bottom target](#Bottom target)
|
||||
|
||||
## Basic heading
|
||||
|
||||
Expected: clicking `Basic heading` scrolls here via case-insensitive heading match.
|
||||
|
||||
## Mixed CASE Heading
|
||||
|
||||
Expected: clicking `Mixed Case` scrolls here (case-insensitive).
|
||||
|
||||
## Scroll padding section 1
|
||||
|
||||
This filler makes scrolling visible.
|
||||
|
||||
## Scroll padding section 2
|
||||
|
||||
This filler makes scrolling visible.
|
||||
|
||||
## Bottom target
|
||||
|
||||
Expected: clicking `Bottom target` from the TOC scrolls near the bottom.
|
||||
Reference in New Issue
Block a user