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

gesture,widget: Text selection in Editor widgets #3

Closed
wants to merge 3 commits into from

Conversation

theclapp
Copy link
Contributor

@theclapp theclapp commented Dec 16, 2020

  • Click-and-drag to select text.
  • Cut/Copy selected text via Shortcut-X/C
  • Paste text via Shortcut-V
  • Paste overwrites selected text
  • Typing overwrites selected text
  • A "select all" hotkey, Shortcut-A
  • Selection via keyboard via Shift-Arrows
  • Touch support
  • Double-click selects a word
  • Triple-click selects a line
  • Shift-click selects from the caret to the clicked position, or adjusts the current selection.

@theclapp
Copy link
Contributor Author

Force-pushed with signed commits. Appreciation to @whereswaldon in #2 (comment) for laying out how to do that.

widget/editor.go Outdated Show resolved Hide resolved
@eliasnaur
Copy link
Contributor

Haven't looked closer yet, but I'd love a test or two to demonstrate and lock in the new behaviour :)

@theclapp
Copy link
Contributor Author

@eliasnaur I added a test case and did some other refactoring. PTAL. Thanks.

@theclapp
Copy link
Contributor Author

theclapp commented Dec 22, 2020

Rebased from main & force-pushed.

@eliasnaur
Copy link
Contributor

Sorry for the delay. I intend to review this soon, but given its complexity I'd like to release the new renderer first.

@theclapp
Copy link
Contributor Author

theclapp commented Dec 28, 2020

There's obviously more to do for this functionality:
[ edit: feature checklist moved to PR description ]

If I have time to work on any of that, would you prefer that I just added it to this branch (probably making the change larger and presumably harder to review), or work on that in a different branch (branched from this one)? Happy to do either, but would prefer to optimize in favor of getting it merged sooner.

@eliasnaur
Copy link
Contributor

Please add new work as separate commits on top of the others. The combined change will be larger, but that's ok as long as the commits are logically separate units.

@theclapp theclapp force-pushed the editor-select branch 4 times, most recently from c21b8ad to 3da1578 Compare December 31, 2020 19:42
@theclapp
Copy link
Contributor Author

Rebased to current main (with the compute renderer) and force-pushed.

Also, earlier, moved the feature list from an earlier comment to the PR description as a checklist.

Copy link
Contributor

@eliasnaur eliasnaur left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent work, I can't wait to have selections in Gio!

This is the first round of review comments. The comments are mainly about making Editor always have a selection (or, equivalently, adding width to the caret), and making the zero value (zero characters selected) useful in the existing operations (move, delete etc.).

widget/editor.go Outdated Show resolved Hide resolved
widget/editor.go Outdated Show resolved Hide resolved
widget/editor.go Outdated Show resolved Hide resolved
widget/editor.go Outdated Show resolved Hide resolved
widget/editor.go Outdated Show resolved Hide resolved
widget/editor.go Outdated Show resolved Hide resolved
widget/editor.go Outdated Show resolved Hide resolved
widget/editor.go Outdated Show resolved Hide resolved
widget/editor.go Outdated Show resolved Hide resolved
widget/label.go Outdated Show resolved Hide resolved
@theclapp
Copy link
Contributor Author

theclapp commented Jan 4, 2021

Pushed some new code. Marked several comments as "resolved". The rest are still outstanding.

@theclapp
Copy link
Contributor Author

theclapp commented Jan 5, 2021

You mentioned "this is the first round of review comments", so maybe you meant to address this later, but just in case: The selection color is hardcoded to color.NRGBA{B: 0xff, A: 0x40} which is suboptimal, to say the least. But I'm not sure where the color should be specified. Any thoughts?

@eliasnaur
Copy link
Contributor

You mentioned "this is the first round of review comments", so maybe you meant to address this later, but just in case: The selection color is hardcoded to color.NRGBA{B: 0xff, A: 0x40} which is suboptimal, to say the least. But I'm not sure where the color should be specified. Any thoughts?

Specifying the color is best left for the theme, which in your case probably means export paintSelection as PaintSelection, calling it from material.Editor. Just like PaintText, PaintSelection would just use op.PaintOp and not specify a color.

widget/editor.go Outdated Show resolved Hide resolved
@theclapp
Copy link
Contributor Author

theclapp commented Jan 6, 2021

[...] The selection color is hardcoded to color.NRGBA{B: 0xff, A: 0x40} [...]

Specifying the color is best left for the theme

I agree.

which in your case probably means export paintSelection as PaintSelection, calling it from material.Editor. Just like PaintText, PaintSelection would just use op.PaintOp and not specify a color.

I'm not sure I know how to do that. (I'm not saying it's impossible, just that I don't know how.) paintSelection calls paint.FillShape, which takes a color explicitly. And where would the color come from in the first place? Can I just add SelectionColor to EditorStyle, and default it to Theme.Palette.ContrastBg in widget/material.Editor(), and pass it to PaintText, where it's used in the paintSelection color? Also, making paintSelection more stand-alone would appear to require iterating over e.shapes again (unless I'm missing something), which seems wasteful.

Copy link
Contributor

@eliasnaur eliasnaur left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

which in your case probably means export paintSelection as PaintSelection, calling it from material.Editor. Just like PaintText, PaintSelection would just use op.PaintOp and not specify a color.

I'm not sure I know how to do that. (I'm not saying it's impossible, just that I don't know how.) paintSelection calls paint.FillShape, which takes a color explicitly.

See Editor.PaintText that uses raw clip and PaintOps for painting the text with whatever material is current.

And where would the color come from in the first place? Can I just add SelectionColor to EditorStyle, and default it to Theme.Palette.ContrastBg in widget/material.Editor()

SGTM

and pass it to PaintText, where it's used in the paintSelection color? Also, making paintSelection more stand-alone would appear to require iterating over e.shapes again (unless I'm missing something), which seems wasteful.

I don't mind the waste for now; it's only iterating the visible lines of text once more. We can get clever in a follow-up, for example by storing into e.selectedShapes just the e.shapes that need selection painted (usually none), or by constructing the selection area as one large clip operation.

widget/editor.go Outdated Show resolved Hide resolved
widget/editor.go Outdated Show resolved Hide resolved
widget/editor.go Show resolved Hide resolved
widget/editor.go Outdated Show resolved Hide resolved
widget/editor.go Outdated Show resolved Hide resolved
@theclapp
Copy link
Contributor Author

Pushed some new code, marked some more conversations as resolved.

Still more work to be done, but getting there.

Pushing the selection-aware code down as far as it could go does in fact make the code cleaner and more streamlined. Thanks for the suggestion.

@theclapp
Copy link
Contributor Author

I think I've addressed all comments / objections / suggestions so far. PTAL. Thank you for your feedback.

widget/editor.go Outdated Show resolved Hide resolved
widget/editor.go Outdated Show resolved Hide resolved
widget/editor.go Outdated Show resolved Hide resolved
widget/editor.go Outdated Show resolved Hide resolved
widget/editor.go Outdated Show resolved Hide resolved
widget/editor.go Outdated Show resolved Hide resolved
widget/editor.go Outdated Show resolved Hide resolved
widget/editor.go Outdated Show resolved Hide resolved
widget/editor.go Outdated Show resolved Hide resolved
widget/editor.go Outdated Show resolved Hide resolved
widget/editor.go Outdated Show resolved Hide resolved
@theclapp
Copy link
Contributor Author

I'm going to bring up the eventual commit order ...

So, for example, a set of N commits that does refactoring only, setting the stage for the selection code, possibly changing the API (e.g. changing Move to MoveCaret, and adding SetCaret), but adding no actual selection code or api, and then another set of N commits that does add the selection code, but makes no (or as few as possible) other changes.

So the first set would, for example, change func (e *Editor) Move(distance int) to func (e *Editor) MoveCaret(caretDelta int), and then the second set would change it to func (e *Editor) MoveCaret(startDelta, endDelta int).

Yes?

So ... would doing all that in place make the comments in this PR hard to read afterwards, or does GH know how to take care of all that? And if the former, maybe I should open a new PR with a new branch with the reordered / combined / split commits? Any advice? I haven't done anything like that before.

@theclapp
Copy link
Contributor Author

Just for posterity: I suspect others might benefit from this as much as I did, which has sections on squashing, splitting, and reordering commits: https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History

@theclapp
Copy link
Contributor Author

One of the things I'm wondering, from you (@eliasnaur) or anyone else that happens to be following along, is: would the reorganization of the commits you're requesting be easier with an interactive rebase, with (I suspect) rather a lot of commit splitting and conflict resolution, or would it be easier if I just diffed the current branch against main, and created brand new commits in a new branch, with (I suspect) way fewer problems with commit splitting and conflict resolution.

I mean, to be sure, the whole diff is 1842 lines, but at least I'd only have to go over them approximately once each, instead of quite possibly several times each. Like I said, looking for advice.

/cc @mvdan @whereswaldon if you have any advice. If not, no worries & thanks anyway. 😄

@mvdan
Copy link
Contributor

mvdan commented Jan 18, 2021

Personally, I just rebase and force-push to PRs :) Whatever you do, "see older versions of this PR" is practically impossible on GitHub, so I go for the simpler approach of fewer commits.

Copy link
Contributor

@eliasnaur eliasnaur left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One of the things I'm wondering, from you (@eliasnaur) or anyone else that happens to be following along, is: would the reorganization of the commits you're requesting be easier with an interactive rebase, with (I suspect) rather a lot of commit splitting and conflict resolution, or would it be easier if I just diffed the current branch against main, and created brand new commits in a new branch, with (I suspect) way fewer problems with commit splitting and conflict resolution.

I mean, to be sure, the whole diff is 1842 lines, but at least I'd only have to go over them approximately once each, instead of quite possibly several times each. Like I said, looking for advice.

FWIW, I rarely start over from a diff to main, and use git rebase -i main almost exclusively to re-order, squash/fixup, edit commits and commit messages.

widget/buffer.go Show resolved Hide resolved
widget/buffer.go Outdated Show resolved Hide resolved
widget/buffer.go Outdated Show resolved Hide resolved
widget/editor.go Show resolved Hide resolved
@whereswaldon
Copy link
Member

I guess my comments are nothing new here, but I also just use git rebase -i for this sort of thing. I have attempted the kind of clean slate history reconstruction that you describe (diffing main), but it's very difficult to retroactively isolate logical changesets with that approach.

@eliasnaur
Copy link
Contributor

I'm going to bring up the eventual commit order ...

So, for example, a set of N commits that does refactoring only, setting the stage for the selection code, possibly changing the API (e.g. changing Move to MoveCaret, and adding SetCaret), but adding no actual selection code or api, and then another set of N commits that does add the selection code, but makes no (or as few as possible) other changes.

Exactly (I'm shamelessly stealing the "setting the stage" metaphor). The goal is to minimize and isolate the exciting changes, to reduce review fatigue and for easier debugging in case a future bisect blames one of your commits.

So the first set would, for example, change func (e *Editor) Move(distance int) to func (e *Editor) MoveCaret(caretDelta int), and then the second set would change it to func (e *Editor) MoveCaret(startDelta, endDelta int).

Yes?

Yes.

So ... would doing all that in place make the comments in this PR hard to read afterwards, or does GH know how to take care of all that? And if the former, maybe I should open a new PR with a new branch with the reordered / combined / split commits? Any advice? I haven't done anything like that before.

Like Daniel, I don't have much hope for retaining the comment history. Even if we did retain history, there's little linking commits to the GitHub PRs. If you feel generous, please do summarize relevant history in the commit messages that end up merged.

@theclapp
Copy link
Contributor Author

Rebased from main before big commit reorganization.

@theclapp theclapp force-pushed the editor-select branch 2 times, most recently from d3d8d6a to ddb58bf Compare January 21, 2021 02:26
@theclapp
Copy link
Contributor Author

I've reorganized the commits into just two, one with just refactoring, and one with just selection code, as requested. PTAL.

Copy link
Contributor

@eliasnaur eliasnaur left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for rebasing the commits ,much appreciated.

I have a single concern about SetCaret left, otherwise LGTM.

widget/editor.go Show resolved Hide resolved
- Move caret from editBuffer.caret to Editor.caret.pos.ofs and related
  refactoring. Move other fields in Editor.caret into Editor.caret.pos.
- Refactor several functions to change a position passed into them,
  rather than changing e.rr.caret directly.
- Add editBuffer.Seek().
- Remove editBuffer.dump().
- Change Editor.Move to MoveCaret.
- Add Editor.SetCaret.
- Updated tests.

Signed-off-by: Larry Clapp <larry@theclapp.org>
- Allow dragging to be on both horizontal and vertical axes at once.
- Split Editor.caret.pos into caret.start and caret.stop. caret.start is
  the old caret.pos, and is both the position of the caret, and also the
  start of selected text. caret.end is the end of the selected text.
  Start can be after end, e.g. after after Shift-DownArrow.
- Update caret.end after a mouse drag, and various shifted keys
  (Shift-UpArrow, Shift-DownArrow, etc).
- Change Shortcut-C to copy only the selected text, not the whole editor
  text.
- Add Shortcut-X to copy and delete selected text, and Shortcut-A to
  select all text.
- The various Insert/Delete/etc functions now overwrite or delete the
  selection, as appropriate.
- Change MoveCaret to accept a distance for selection end, as well.
  Change SetCaret to accept a selection end offset.
- Add SelectionLen to get the selection length, Selection to get
  selection offsets, SelectedText to get the selected text, and
  ClearSelection to clear the selection.
- Add a rudimentary selection unit test, and extend the deleteWord unit
  test with some text selection cases.
- Add SelectionColor to material.EditorStyle, which defaults to
  Theme.Palette.ContrastBg.

Signed-off-by: Larry Clapp <larry@theclapp.org>
@lrewega
Copy link

lrewega commented Jan 22, 2021

Trying out the latest commit 6502c73 I noticed that using arrow keys with highlighted text does not clear the highlight, as I would expect, but instead slides the highlight, which is a behaviour I've never seen before!

@eliasnaur
Copy link
Contributor

Trying out the latest commit 6502c73 I noticed that using arrow keys with highlighted text does not clear the highlight, as I would expect, but instead slides the highlight, which is a behaviour I've never seen before!

I'm seeing this too. It's an interesting effect, for sure :)

@theclapp
Copy link
Contributor Author

using arrow keys with highlighted text does not clear the highlight, as I would expect, but instead slides the highlight

Yep. Good catch! This is only for the left & right arrows; up & down clear the selection as expected.

It was like this before the commit reorganization, too, apparently.

This is a direct result of the MoveCaret(startDelta, endDelta int) api. There's no value of endDelta you can send to extend the selection if you've pressed Shift and to clear it if you haven't. A 0 will shrink/extend the selection, a +/-1 will slide/shrink/enlarge it, depending on the sign of startDelta. (Not noticing this bug was a direct result of me always either clicking the mouse or using the up/down arrows during testing, I guess. And insufficient test cases.)

@eliasnaur, what are your thoughts? As we've discussed, we want the api to be selection-agnostic as far down as possible. The options as I see them:

  1. Change MoveCaret to accept another flag that says clear or extend the selection, like the internal moveWord/moveLines/etc, and ignore endDelta if it's selectionClear. MoveCaret(startDelta, endDelta int, selAct SelectionAction).
  2. Call ClearSelection before MoveCaret if Shift is pressed
  3. Have two functions: MoveCaret(startDelta) which calls ClearSelection internally and then moves the caret, and MoveCaretShifted(startDelta, endDelta) (or possibly ChangeSelection or something else?) which does what MoveCaret does now
  4. Pass an out-of-band value in endDelta (e.g. math.MaxInt32) that's special-cased to clear the selection.
const NoSelection = math.MaxInt32

func (e *Editor) MoveCaret(startDelta, endDelta int) {
  if endDelta == NoSelection { e.ClearSelection() }
  // ...

2-4 still require selection-specific code in the left/right arrow code, though, and also user code would have to do the same thing in a similar context.

I think I prefer option 2, with the option of changing to option 1 when/if we ever export the other movement functions (moveWord/moveLine/etc).

@eliasnaur
Copy link
Contributor

using arrow keys with highlighted text does not clear the highlight, as I would expect, but instead slides the highlight

Yep. Good catch! This is only for the left & right arrows; up & down clear the selection as expected.

This is a direct result of the MoveCaret(startDelta, endDelta int) api. There's no value of endDelta you can send to extend the selection if you've pressed Shift and to clear it if you haven't. A 0 will shrink/extend the selection, a +/-1 will slide/shrink/enlarge it, depending on the sign of startDelta. (Not noticing this bug was a direct result of me always either clicking the mouse or using the up/down arrows during testing, I guess. And insufficient test cases.)

@eliasnaur, what are your thoughts? As we've discussed, we want the api to be selection-agnostic as far down as possible. The options as I see them:

2. Call ClearSelection before MoveCaret if Shift is pressed

I believe you mean when Shift is not pressed?

I think I prefer option 2

I prefer option 2 as well. I don't see a need for MoveCaret to precisely map to keyboard combinations, so a ClearSelection when moving without shift is fine.

with the option of changing to option 1 when/if we ever export the other movement functions (moveWord/moveLine/etc).

I think we can fold moveWord/Line into a generalization of MoveCaret. See my #3 (comment) sketch.

@theclapp
Copy link
Contributor Author

I believe you mean when Shift is not pressed?

Right. Oops.

option 2

Awesome.

I think we can fold moveWord/Line into a generalization of MoveCaret

For a later PR.

Make the left and right arrow keys dismiss the selection. (Up and down
already did.) Add a test case for all four.

Signed-off-by: Larry Clapp <larry@theclapp.org>
@theclapp
Copy link
Contributor Author

theclapp commented Jan 23, 2021

using arrow keys with highlighted text does not clear the highlight

Fixed. Test case added, too.

@eliasnaur
Copy link
Contributor

Great work, all merged! Thank you for your patience.

@eliasnaur eliasnaur closed this Jan 24, 2021
theclapp added a commit to theclapp/gio that referenced this pull request Jan 16, 2023
- This is the 1st commit message:

ScrollTo, PagePrev/Next, etc

This is a combination of 5 commits.

- layout: add List.ScrollTo, ScrollPages, and 2 more

Also add PagePrev & PageNext.

Include tests for all.

- app,app/internal/window,io/system: macOS menus

A strawman interface to the macOS menuing system.

- Make menus compile on other architectures

- app/internal/window: Add modifiers to macOS menu event

- Update to latest Gio main

- This is the commit message gioui#2:

Delete app/internal/wm/window.go

No longer used in main branch.

- This is the commit message gioui#3:

Move menu changes into their own files.

- This is the commit message gioui#4:

io/key,widget: Make editor hotkey filter more specific

Only look for up/down/pageup/pagedown if the editor is multiline.

Only look for enter/return if the editor is Submit == true.

Fix modifier for delete-backward & forward.

Implement some key.Set builder functions and use them in the above code.

Fixes: https://todo.sr.ht/~eliasnaur/gio/399

- This is the commit message gioui#5:

widget: Tweak editor hotkey event listener

For delete-forward and backward, only listen for ShortAlt.

Only listen for Short-[C,X] (copy selection and cut selection) when
there's a selection.

- This is the commit message gioui#6:

Add system.MenuEvent to Window.processEvent

- This is the commit message gioui#7:

key: rename BuildKeySet and BuildKeyGroup

to BuildKeyset and BuildKeygroup.

- This is the commit message gioui#8:

Sleep 2ms after onClose.

Signed-off-by: Larry Clapp <larry@theclapp.org>
theclapp added a commit to theclapp/gio that referenced this pull request Jan 12, 2024
- This is the 1st commit message:

ScrollTo, PagePrev/Next, etc

This is a combination of 5 commits.

- layout: add List.ScrollTo, ScrollPages, and 2 more

Also add PagePrev & PageNext.

Include tests for all.

- app,app/internal/window,io/system: macOS menus

A strawman interface to the macOS menuing system.

- Make menus compile on other architectures

- app/internal/window: Add modifiers to macOS menu event

- Update to latest Gio main

- This is the commit message gioui#2:

Delete app/internal/wm/window.go

No longer used in main branch.

- This is the commit message gioui#3:

Move menu changes into their own files.

- This is the commit message gioui#4:

io/key,widget: Make editor hotkey filter more specific

Only look for up/down/pageup/pagedown if the editor is multiline.

Only look for enter/return if the editor is Submit == true.

Fix modifier for delete-backward & forward.

Implement some key.Set builder functions and use them in the above code.

Fixes: https://todo.sr.ht/~eliasnaur/gio/399

- This is the commit message gioui#5:

widget: Tweak editor hotkey event listener

For delete-forward and backward, only listen for ShortAlt.

Only listen for Short-[C,X] (copy selection and cut selection) when
there's a selection.

- This is the commit message gioui#6:

Add system.MenuEvent to Window.processEvent

- This is the commit message gioui#7:

key: rename BuildKeySet and BuildKeyGroup

to BuildKeyset and BuildKeygroup.

- This is the commit message gioui#8:

Sleep 2ms after onClose.

Signed-off-by: Larry Clapp <larry@theclapp.org>
@gioui gioui deleted a comment from ddkwork Apr 11, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
5 participants