Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add infrastructure to Parley to facilitate text selection/editing #52

Open
PoignardAzur opened this issue May 11, 2024 · 12 comments
Open
Assignees
Labels
enhancement New feature or request

Comments

@PoignardAzur
Copy link
Contributor

PoignardAzur commented May 11, 2024

We want Parley to implement types and methods that Masonry and other editors will be able to use to create a text-editing widget, or to handle selection in non-editable text.

After some discussion we've settled on the following:

  • A TextMovement enum representing different possible changes to a text edit's selection and content. Possible variants might be eg MoveLeft, MoveToNextWord, SelectToNextParagraph, RemovePreviousWord, etc.
  • A function that maps a String, a cursor position(/span) and a TextMovement to a StringDiff and position(/span). That function essentially tells you "if you do that, here's how the text will change and where your cursor will be next".
  • Later down the road, once we want to implement spans, a function that maps a span and a StringDiff to an optional span.

Possible signatures:

enum TextMovement {
  MoveLeft,
  MoveRight,
  // ...
}

struct StringDiff {
  removed_span: usize..usize,
  new_position: usize,
  added_text: String,
}

fn apply_movement(string: &str, movement: TextMovement, byte_offset: usize) -> StringDiff;

fn map_span(span: usize..usize, diff: StringDiff) -> Option<usize..usize>;

We might use Parley's Cursor type instead of byte offsets in these interfaces.

@xorgy xorgy self-assigned this May 14, 2024
@nicoburns nicoburns added the enhancement New feature or request label May 28, 2024
@PoignardAzur
Copy link
Contributor Author

We might use Parley's Cursor type instead of byte offsets in these interfaces.

Never mind. A quick look at parley suggests Cursor is more of an output type than a live marker.

Basically, you have one function that gives you a cursor from a (x, y) pair, one function that gives you a cursor from a offset: size_t, is_leading: bool pair, and then code in Masonry that uses the values returned to display stuff.

You're not supposed to feed it back in other parley functions.

@PoignardAzur
Copy link
Contributor Author

PoignardAzur commented Aug 15, 2024

Planned API:

pub enum WritingDirection {
    LeftToRight,
    RightToLeft,
}

pub enum ByteDirection {
    Upstream,
    Downstream,
}

pub enum Movement {
    Grapheme(ByteDirection),
    Word(ByteDirection),
    Line(ByteDirection),
    ParagraphStart,
    ParagraphEnd,
    LineUp,
    LineDown,
    PageUp,
    PageDown,
    DocumentStart,
    DocumentEnd,
}

impl<B> Layout<b> {
    fn get_direction(&self, index: usize, affinity: ByteDirection) -> WritingDirection;
}

impl WritingDirection {
    fn left() -> ByteDirection;
    fn right() -> ByteDirection;
}

impl<B> Layout<b> {
    fn apply_movement(&self, index: usize, affinity: ByteDirection, x_pos: f64, movement: Movement) -> (usize, ByteDirection);
}

On the Masonry side:

struct Selection {
    anchor_index: usize,
    anchor_affinity: ByteDirection,
    active_index: usize,
    active_affinity: ByteDirection,
    x_pos: f64,
}

impl Selection {
    fn move(&mut self, layout: &Layout, movement: Movement);
    fn select(&mut self, layout: &Layout, movement: Movement);
    fn is_caret(&self) -> bool;
}

enum TextAction {
    // Arrows, Home, End, etc
    Move(Movement),
    // Ctrl+A
    SelectAll,
    // Shift+Arrows, Shift+Home, Shift+End, etc
    Select(Movement),
    // Backspace, Delete, Ctrl+Backspace, Ctrl+Delete
    SelectAndDelete(Movement),
    // Regular input, paste, IME
    Splice(String),
}

fn convert(event: winit::KeyEvent) -> TextAction;

Will take a lot of inspiration from existing code, especially movement.rs in Masonry.

@PoignardAzur
Copy link
Contributor Author

Re-reading this, this is the most beautiful API I've written in my freaking life.

@dfrg
Copy link
Collaborator

dfrg commented Aug 15, 2024

Looks good. I don't see any reason why those selection and action types couldn't also live in parley though.

@dfrg
Copy link
Collaborator

dfrg commented Aug 15, 2024

We might use Parley's Cursor type instead of byte offsets in these interfaces.

Never mind. A quick look at parley suggests Cursor is more of an output type than a live marker.

Basically, you have one function that gives you a cursor from a (x, y) pair, one function that gives you a cursor from a offset: size_t, is_leading: bool pair, and then code in Masonry that uses the values returned to display stuff.

You're not supposed to feed it back in other parley functions.

Feeding it back into the API was certainly the intended purpose. This is why CursorPath exists.. it gives you quick access to the internal data structures to handle things like movement and generating selection geometry.

@xorgy
Copy link
Member

xorgy commented Aug 15, 2024

If Cursor is desired for a new type, Cursor can be renamed to something like Hit. It's more intended for low level hit testing stuff at this point; but it was originally meant to be a long-lived type that could be fed back into the layout it came from.

@dfrg
Copy link
Collaborator

dfrg commented Aug 15, 2024

In reality, the type we care about is selection, which I would implement as:

struct Selection {
    anchor: Cursor,
    focus: Cursor,
    h_pos: Option<f32>,
}

Every movement or edit action would take an input selection and action and then return a new selection (which may be collapsed).

@PoignardAzur
Copy link
Contributor Author

Remind me what collapsed selections are?

@DJMcNab
Copy link
Member

DJMcNab commented Aug 15, 2024

I would guess that a collapsed cursor is a cursor where the anchor and focus are the same location - i.e. a caret cursor.

@dfrg
Copy link
Collaborator

dfrg commented Aug 15, 2024

Yeah, what Daniel said. If I remember correctly, the web cursor API refers to that as “collapsed”

@waywardmonkeys
Copy link
Contributor

https://developer.mozilla.org/en-US/docs/Web/API/Selection/collapse

@PoignardAzur
Copy link
Contributor Author

After some discussion with Chad, here is the second iteration of the API I'd propose:

pub enum LogicalDirection {
    Upstream,
    Downstream,
}

pub enum Movement {
    Backspace, // Backspace behaves somewhat differently than LeftArrow. 
    Grapheme(LogicalDirection),
    Word(LogicalDirection),
    Line(LogicalDirection),
    ParagraphStart,
    ParagraphEnd,
    LineUp(usize),
    LineDown(usize),
    DocumentStart,
    DocumentEnd,
}

impl<B> Layout<b> {
    fn logical_left(&self, index: usize, affinity: LogicalDirection) -> LogicalDirection;
    fn logical_right(&self, index: usize, affinity: LogicalDirection) -> LogicalDirection;

    fn index_and_affinity_from_point(&self, x: f32, y: f32) -> (usize, LogicalDirection);
    fn cursor_from_index(index: usize, affinity: LogicalDirection);

    fn apply_movement(&self, index: usize, affinity: LogicalDirection, x_pos: f64, movement: Movement) -> (usize, ByteDirection);
}

And Masonry would take care of the rest for now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

6 participants