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

Cursor drift in Gtk.Entry in Todo example app #53

Open
niteria opened this issue Jul 23, 2019 · 7 comments
Open

Cursor drift in Gtk.Entry in Todo example app #53

niteria opened this issue Jul 23, 2019 · 7 comments

Comments

@niteria
Copy link

niteria commented Jul 23, 2019

If you take the app built in https://haskell-at-work.com/episodes/2019-01-10-purely-functional-gtk-part-1-hello-world.html and https://haskell-at-work.com/episodes/2019-01-19-purely-functional-gtk-part-2-todo-mvc.html it manifests the following issue:

Peek 2019-07-23 12-35

After I selected the text I started typing "123", but the cursor moved to the beginning after I typed "1" and I got "231".

The code for the app looks resonable, that's why I raise this issue here.

I have the app in a buildable form at niteria@2ccd690

@niteria
Copy link
Author

niteria commented Jul 24, 2019

I did some debugging and I believe I know what's going on here.

First, here are some things I experimentally discovered about Gtk.Entry:

  • Selecting everything and typing "1" produces 2 EditableChanged signals. The Gtk.Entry moves sequentially through these states:

    1. old contents
    2. an empty string as a content
    3. having "1" as content.
  • If you Gtk.setEntryText to the text that Gtk.Entry already has, the cursor position won't change. Otherwise, it changes the text and moves the cursor to position 0.

Now here's what happens in gi-gtk-declarative:

  1. We start out with Gtk.Entry having some contents.
  2. When we select and replace with "1" two signals are generated. First when the contents are "" and second when the contents are "1". This results in 2 state changes (the update' function) and 2 view changes (the view' function).
  3. Now here's where things get funny. When we process the first view change inside the Patchable instance for SingleWidget the actual contents of Gtk.Entry are already "1" and we change it back to "", resetting the cursor to position 0.
  4. When we process the next view we change the contents back to the correct "1", but unfortunately the cursor position is long gone.

@owickstrom
Copy link
Owner

@niteria Thanks for the comprehensive debugging work! This is indeed problematic. Have you found any way of receiving an "atomic" text change event? I'm guessing that trying to detect what events are relevant (if we say that the "" event when replacing text is irrelevant) is going to be very brittle.

@owickstrom
Copy link
Owner

I think a slightly more reasonable cursor position after replacing would be at the end. But can we detect that an insertion of text actually followed a "" event, and that it was logically a replacement event?

niteria added a commit to niteria/gi-gtk-declarative that referenced this issue Jul 26, 2019
@niteria
Copy link
Author

niteria commented Jul 26, 2019

I was able to work around this by routing the incoming event to a skip channel (channel that only keeps the last value) that is consumed by a producer pipe that reemits deduplicated events back into the app.

The way the deduplication works is that the pipe thread (blocking) reads from the channel, waits for a millisecond, and tries to (non-blocking) read from the channel again. It returns the last successful read.

This is pretty ugly and annoying to scale to more inputs, but I can confirm that it works.

What I'm considering doing is to modify the App Simple model to do the 1ms wait in the core loop and pass an ordered list of events to the update function. Then the client can deduplicate as needed.

@owickstrom
Copy link
Owner

That does sound quite ugly. I hope we can find a way to interact with the widget's signals so that we don't have to mess around with this level of detail.

Looking at https://stackoverflow.com/a/3878872, they seem to discuss the same thing, and this answer quotes:

The ::changed signal is emitted at the end of a single user-visible operation on the contents of the GtkEditable.

E.g., a paste operation that replaces the contents of the selection will cause only one signal emission (even though it is implemented by first deleting the selection, then inserting the new content, and may cause multiple ::notify::text signals to be emitted).

This suggests that it shouldn't emit two events on changed. But that's what you got, right?

Maybe notify:text could be used instead.

@niteria
Copy link
Author

niteria commented Jul 28, 2019

I've tried a simple program of the form:

void $ Gtk.onEditableChanged entry $ do                                                                                                                                                                         
  str <- Gtk.entryGetText entry                                                                                                                                                                                 
  putStrLn $ "onEditableChanged " ++ show str       

The answer on the StackOverflow is right that a paste produces one line, but unfortunately, replacement (pressing a key after selecting all text) produces two.

I also have a simple example (https://github.com/niteria/gi-gtk-declarative/blob/cursor-drift/examples/LaggyUIDemo.hs) that demonstrates a similar problem if the render takes nontrivial time then the cursor will drift.

The example in the MVar page that introduces Skip Channels suggests using them for fast-changing signals like mouse cursor position. I suspect that if I created an example that displays mouse cursor position in a Label I'd run into performance problems.

@marhop
Copy link
Collaborator

marhop commented Dec 30, 2020

FWIW, a workaround in application code may be to use the keyReleaseEvent signal instead of changed.

This requires a slightly more complicated event handler and of course the two signals have different semantics: An obvious difference is that keyReleaseEvent fires more often than changed (e.g., when using the cursor keys to move inside an entry field - keys get released but nothing changes); there may be more subtle or potentially dangerous effects, so use with caution.

Anyway, here is a complete example in case someone finds this useful.

{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}

import Control.Monad (void)
import Data.Text (Text)
import GI.Gdk.Structs.EventKey (EventKey)
import qualified GI.Gtk as Gtk
import GI.Gtk.Declarative
import GI.Gtk.Declarative.App.Simple

data State = State Text

data Event = TextChanged Text | Closed

view' :: State -> AppView Gtk.Window Event
view' (State t) =
  bin Gtk.Window [on #deleteEvent (const (True, Closed))] $ entry t

update' :: State -> Event -> Transition State Event
update' _ (TextChanged t) = Transition (State t) (pure Nothing)
update' _ Closed = Exit

main :: IO ()
main =
  void $
    run
      App
        { view = view',
          update = update',
          inputs = [],
          initialState = State ""
        }

This implementation of entry uses the changed signal and produces the annoying cursor drift described above ...

entry :: Text -> Widget Event
entry t = widget Gtk.Entry [#text := t, onM #changed toEvent]
  where
    toEvent :: Gtk.Entry -> IO Event
    toEvent e = TextChanged <$> Gtk.entryGetText e

... whereas this implementation uses the keyReleaseEvent signal and exhibits "normal" behaviour. No weird cursor drift here!

entry :: Text -> Widget Event
entry t = widget Gtk.Entry [#text := t, onM #keyReleaseEvent toEvent]
  where
    toEvent :: EventKey -> Gtk.Entry -> IO (Bool, Event)
    toEvent _ e = (False,) . TextChanged <$> Gtk.entryGetText e

Cheers,
Martin

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

No branches or pull requests

3 participants