~eliasnaur/gio

4 2

pass-through pointer.Press events, or, how to overlay pointer handlers

Details
Message ID
<4eaccbbd51c78469ffd0a6a7fdbb2db7@riseup.net>
DKIM signature
pass
Download raw message
Hi all,
I am trying to implement a "Long Press" io/pointer handler to enable
paste
action via a long-press (mostly for android) into a widget.Editor. I'd
like to
receive all the pointer events, but pass them through to the handler
'below'
the hit area.

I thought I might be able to use pointer.PassOp to let pointer.Press
events
pass through to the widget.Editor; but I think this wont work based on
my
reading of io/pointer, so I tried using op.Save()/Load() around the
AreaOp.Add
and PointerOp.Add calls but I'm afraid that didn't seem to work. I am
guessing
this is something rather simple, but I think I am misunderstanding how
the
stack operations apply to AreaOp/InputOp.

Here's what I wrote (it's a bit of a kludge and i haven't commited it
yet since
it doesn't actually work).

"""
type LongPressType uint8

const (
        LongPressed LongPressType = iota
        LongPressCancelled
)

// LongPressEvent represent a long press action
type LongPressEvent struct {
        Type LongPressType
}

// LongPress detects a press-and-hold in the form of LongPress events
type LongPress struct {
        pressedAt time.Time
        // releasedAt tracks the pointer
        releasedAt time.Time
        // pressedFor tracks how long the press has been held so far.
        pressedFor time.Duration
        // detectAt tracks how long the press must be held.
        detectAt time.Duration
        // pressing tracks whether a press is occurring
        pressed bool
        timer *time.Timer
        // callback is called when the timer fires
        callback func()
}

// Add the handler to the operation list to receive click events.
func (l *LongPress) Add(ops *op.Ops) {
        // XXX: for some reason I think PassOp must op.Add first?
        pointer.PassOp{Pass: true}.Add(ops)
        op := pointer.InputOp{
                Tag:   l,
                Types: pointer.Press | pointer.Release | pointer.Enter |
pointer.Leave | pointer.Drag,
        }
        op.Add(ops)
}

// Events returns the next click event, if any.
func (l *LongPress) Events(q event.Queue) []LongPressEvent {
        var events []LongPressEvent
        // consume pointer events and start or stop a timer
        for _, evt := range q.Events(l) {
                e, ok := evt.(pointer.Event)
                if !ok {
                        continue
                }
                switch e.Type {
                case pointer.Press:
                        l.pressedAt = time.Now()
                        l.timer = time.NewTimer(l.detectAt)
                        time.AfterFunc(l.detectAt, l.callback)
                        l.pressed = true
                case pointer.Cancel, pointer.Release, pointer.Leave,
pointer.Drag:
                        if l.pressed {
                                l.pressed = false
                                l.pressedFor =
time.Now().Sub(l.pressedAt)
                                if !l.timer.Stop() {
                                        <-l.timer.C
                                }
                                l.timer = nil
                                events = append(events,
LongPressEvent{Type: LongPressCancelled})
                        }
                case pointer.Enter:
                }
        }

        // check if the timer has fired return a LongPressEvent
        if l.timer != nil {
                select {
                case t := <-l.timer.C:
                        l.pressedFor = t.Sub(l.pressedAt)
                        if l.pressed {
                                l.pressed = false
                                l.pressedFor =
time.Now().Sub(l.pressedAt)
                                events = append(events,
LongPressEvent{Type: LongPressed})
                        }
                default:
                        // update pressedFor each call to Events()
                        //t := time.Now()
                        //l.pressedFor = t.Sub(al.pressedAt)
                }
        }
        return events
}

// NewLongPress returns a LongPress that triggers after the duration
func NewLongPress(cb func(), duration time.Duration) *LongPress {
        return &LongPress{detectAt: duration, callback: cb}
}
"""

this is used like so (some context omitted but should convey the idea)

"""
... snip ...
// note that the app.window.Invalidate method is passed as a callback to
wake up
// the event loop on a timer firing, this is probably not the best way
to do this
paste := NewLongPress(a.w.Invalidate, 800*time.Millisecond)
dims := material.Editor(th, c.compose, "").Layout(gtx)
state := op.Save(gtx.Ops)
a := pointer.Rect(image.Rectangle{Max: dims.Size})
a.Add(gtx.Ops)
paste.Add(gtx.Ops)
state.Load()
... snip ...
"""

Thank you for looking! And I really appreciate the help I've received
with other
issues and for the gio project.

-M
Details
Message ID
<1e2be3e0-718a-4ca6-95a3-a6d67623cb39@www.fastmail.com>
In-Reply-To
<4eaccbbd51c78469ffd0a6a7fdbb2db7@riseup.net> (view parent)
DKIM signature
missing
Download raw message
I have similar feature on my app. A long-press open a small dialog to "copy" and "paste", into some Editor. You need to use `pointer.PassOp{Pass: true}.Add(gtx.Ops)`.

I think you should do something like:

paste := NewLongPress(a.w.Invalidate, 800*time.Millisecond)

dims := material.Editor(th, c.compose, "").Layout(gtx)

state := op.Save(gtx.Ops)
pointer.PassOp{Pass: true}.Add(gtx.Ops) // << Here
pointer.Rect(image.Rectangle{Max: dims.Size}).Add(gtx.Ops)
paste.Add(gtx.Ops)
state.Load()

You use the `pointer.PassOp` to not block the click, so the Editor can receive them too. The `Stack/State` is needed to prevent "leak" the pointer.Rect. I think it will be enough. 

-- 
  Lucas Rodrigues
  inkeliz@inkeliz.com

On Mon, Jun 21, 2021, at 3:08 PM, masala@riseup.net wrote:
> Hi all,
> I am trying to implement a "Long Press" io/pointer handler to enable
> paste
> action via a long-press (mostly for android) into a widget.Editor. I'd
> like to
> receive all the pointer events, but pass them through to the handler
> 'below'
> the hit area.
> 
> I thought I might be able to use pointer.PassOp to let pointer.Press
> events
> pass through to the widget.Editor; but I think this wont work based on
> my
> reading of io/pointer, so I tried using op.Save()/Load() around the
> AreaOp.Add
> and PointerOp.Add calls but I'm afraid that didn't seem to work. I am
> guessing
> this is something rather simple, but I think I am misunderstanding how
> the
> stack operations apply to AreaOp/InputOp.
> 
> Here's what I wrote (it's a bit of a kludge and i haven't commited it
> yet since
> it doesn't actually work).
> 
> """
> type LongPressType uint8
> 
> const (
>         LongPressed LongPressType = iota
>         LongPressCancelled
> )
> 
> // LongPressEvent represent a long press action
> type LongPressEvent struct {
>         Type LongPressType
> }
> 
> // LongPress detects a press-and-hold in the form of LongPress events
> type LongPress struct {
>         pressedAt time.Time
>         // releasedAt tracks the pointer
>         releasedAt time.Time
>         // pressedFor tracks how long the press has been held so far.
>         pressedFor time.Duration
>         // detectAt tracks how long the press must be held.
>         detectAt time.Duration
>         // pressing tracks whether a press is occurring
>         pressed bool
>         timer *time.Timer
>         // callback is called when the timer fires
>         callback func()
> }
> 
> // Add the handler to the operation list to receive click events.
> func (l *LongPress) Add(ops *op.Ops) {
>         // XXX: for some reason I think PassOp must op.Add first?
>         pointer.PassOp{Pass: true}.Add(ops)
>         op := pointer.InputOp{
>                 Tag:   l,
>                 Types: pointer.Press | pointer.Release | pointer.Enter |
> pointer.Leave | pointer.Drag,
>         }
>         op.Add(ops)
> }
> 
> // Events returns the next click event, if any.
> func (l *LongPress) Events(q event.Queue) []LongPressEvent {
>         var events []LongPressEvent
>         // consume pointer events and start or stop a timer
>         for _, evt := range q.Events(l) {
>                 e, ok := evt.(pointer.Event)
>                 if !ok {
>                         continue
>                 }
>                 switch e.Type {
>                 case pointer.Press:
>                         l.pressedAt = time.Now()
>                         l.timer = time.NewTimer(l.detectAt)
>                         time.AfterFunc(l.detectAt, l.callback)
>                         l.pressed = true
>                 case pointer.Cancel, pointer.Release, pointer.Leave,
> pointer.Drag:
>                         if l.pressed {
>                                 l.pressed = false
>                                 l.pressedFor =
> time.Now().Sub(l.pressedAt)
>                                 if !l.timer.Stop() {
>                                         <-l.timer.C
>                                 }
>                                 l.timer = nil
>                                 events = append(events,
> LongPressEvent{Type: LongPressCancelled})
>                         }
>                 case pointer.Enter:
>                 }
>         }
> 
>         // check if the timer has fired return a LongPressEvent
>         if l.timer != nil {
>                 select {
>                 case t := <-l.timer.C:
>                         l.pressedFor = t.Sub(l.pressedAt)
>                         if l.pressed {
>                                 l.pressed = false
>                                 l.pressedFor =
> time.Now().Sub(l.pressedAt)
>                                 events = append(events,
> LongPressEvent{Type: LongPressed})
>                         }
>                 default:
>                         // update pressedFor each call to Events()
>                         //t := time.Now()
>                         //l.pressedFor = t.Sub(al.pressedAt)
>                 }
>         }
>         return events
> }
> 
> // NewLongPress returns a LongPress that triggers after the duration
> func NewLongPress(cb func(), duration time.Duration) *LongPress {
>         return &LongPress{detectAt: duration, callback: cb}
> }
> """
> 
> this is used like so (some context omitted but should convey the idea)
> 
> """
> ... snip ...
> // note that the app.window.Invalidate method is passed as a callback to
> wake up
> // the event loop on a timer firing, this is probably not the best way
> to do this
> paste := NewLongPress(a.w.Invalidate, 800*time.Millisecond)
> dims := material.Editor(th, c.compose, "").Layout(gtx)
> state := op.Save(gtx.Ops)
> a := pointer.Rect(image.Rectangle{Max: dims.Size})
> a.Add(gtx.Ops)
> paste.Add(gtx.Ops)
> state.Load()
> ... snip ...
> """
> 
> Thank you for looking! And I really appreciate the help I've received
> with other
> issues and for the gio project.
> 
> -M
> 
Details
Message ID
<cb218d45a4e1a175cd551a5a988bd26b@riseup.net>
In-Reply-To
<1e2be3e0-718a-4ca6-95a3-a6d67623cb39@www.fastmail.com> (view parent)
DKIM signature
pass
Download raw message
On 2021-06-21 17:01, Lucas Rodrigues wrote:
> I have similar feature on my app. A long-press open a small dialog to
> "copy" and "paste", into some Editor. You need to use
> `pointer.PassOp{Pass: true}.Add(gtx.Ops)`.
> 
> I think you should do something like:
> 
> paste := NewLongPress(a.w.Invalidate, 800*time.Millisecond)
> 
> dims := material.Editor(th, c.compose, "").Layout(gtx)
> 
> state := op.Save(gtx.Ops)
> pointer.PassOp{Pass: true}.Add(gtx.Ops) // << Here
> pointer.Rect(image.Rectangle{Max: dims.Size}).Add(gtx.Ops)
> paste.Add(gtx.Ops)
> state.Load()
> 
> You use the `pointer.PassOp` to not block the click, so the Editor can
> receive them too. The `Stack/State` is needed to prevent "leak" the
> pointer.Rect. I think it will be enough.
> 
> -- 
>   Lucas Rodrigues
>   inkeliz@inkeliz.com

Ok! That worked. I am a bit confused but I think the main difference is
that
the PassOp is being applied to the AreaOp - so that the events then
reach the
material.Editor's "hit area" - ie:

...
(stack growing downlwards)
| <- stack saved
PassOp
|
AreaOp <- from "LongPress"
|
InputOp
| <- stack restored
AreaOp  <- from widget.Editor
|
InputOp
...

(Ok, probably a bad rendering...)

I am afraid I dont have a good visualization of how the op.Save()/Load()
is working, or why this is necessary, though. Time to read more I guess
:)

p.s. src for the project is:
https://github.com/katzenpost/catchat/tree/wip_gio_interface

if your project is also FOSS please share - I am curious how you wakeup
upon a timer fire or otherwise detect specific intervals, I would
ideally
like to not have to pass a pointer to the window.Invalidate method.

Thanks again
-M
Details
Message ID
<81a6911e-96c9-40aa-aa49-a38c99815dbf@www.fastmail.com>
In-Reply-To
<cb218d45a4e1a175cd551a5a988bd26b@riseup.net> (view parent)
DKIM signature
missing
Download raw message
Unfurtunelly, it's not open source, but I can share some code. 

What happens is that your pointer.Area is overlaying the Editor, so 
imagine like a Z-Index: 

Z-Index 1: Editor 
Z-Index 2: LongPress (Your custom pointer.Area) 

By default, only one pointer.Area receives the events, which is the last 
one. The pointer.PassOp allows passing the events to the 
next one, in that case: "Z-Index 1". Now, both can receive events.

-------------

You don't need to use `window.Invalidate`, you should use 
`op.InvalidateOp{}.Add(gtx.Ops)` instead. 

However, you may need to set the "At" on `op.InvalidateOp{}`, so it will 
"wake at some point in the future". 

If you call: `op.InvalidateOp{}.Add(gtx.Ops)` it will generate a new 
frame, when the current frame is done. It's good for animations. In the 
end: it will generate a new frame, each 16ms, if you set that op each 
frame. 

If you call `op.InvalidateOp{At: time.Now().Add(300*time.Millisecond 
)}.Add(gtx.Ops)` it will wait 300 milliseconds, and then Gio will 
generate a new frame. That might be what you want. Since you need to 
check if the user keeps pressing after some amount of time. 

The window.Invalidate should be used when it happens outside of the 
frame, such as a networking event (receive a response from 
http.Get("..."), then you call window.Invalidate). 

--------- 

That is what I use (it is since the my custom element.Input): 

```
for _, ev := range gtx.Queue.Events(&e.slider) {
	evt, ok := ev.(pointer.Event)
	if !ok {
		continue
	}

	switch evt.Source {
	case pointer.Mouse:
		switch {
		case evt.Type == pointer.Press && evt.Buttons == pointer.ButtonPrimary:
			if !e.Editor.Focused() {
				e.Editor.Focus()
			}
		case evt.Type == pointer.Press && evt.Buttons == pointer.ButtonSecondary: // Right Click
			if !e.Editor.Focused() {
				e.Editor.Focus()
			}
			e.slider.Toggle()
		}
	case pointer.Touch:
		switch evt.Type {
		case pointer.Press:
			if !e.Editor.Focused() {
				e.Editor.Focus()
			}
			e.sliderMobilePress = gtx.Now  
		case pointer.Release:
			e.sliderMobilePress = time.Time{}
		case pointer.Cancel:
			e.sliderMobilePress = time.Time{}
		}
	}
}

if e.sliderMobilePress != (time.Time{}) {
	if gtx.Now.Sub(e.sliderMobilePress) >= 300*time.Millisecond {
		if !e.Editor.Focused() {
			e.Editor.Focus()
		}
		e.slider.Toggle()
		e.sliderRequested = true
		e.sliderMobilePress = time.Time{}
	} else {
		// gutils.Vibrate(gtx.Ops)
		gutils.InvalidateAfter(gtx.Ops, 300*time.Millisecond)
	}
}
```

The ` gutils.InvalidateAfter` calls the `op.InvalidateOp{}`, the ` 
e.slider` is a custom `elements.Slider` which is used to display the 
"Copy" and "Paste". The `e.sliderMobilePress` is a time.Time, so
it check when the user clicks, if it's larger than 300 miliseconds
it will open. The e.sliderMobilePress is reseted to `time.Time{}`
when the user releases the button, or open the slider.

The layout is rendered as: 

gutils.Stack(gtx, func(gtx layout.Context) { 
call.Add(gtx.Ops) // << Editor 
}) 

e.drawBorder(gtx) // << Borders 
e.drawCover(gtx) // << Icons 
e.drawSlider(gtx) // << Copy/Paste button slider 
e.drawSliderInvisibleButton(gtx) // << The pointer.Area (...) that gets 
the clicks to open the slider, it uses `op.PassOp`. ;) 
```

If you notice, my code uses `e.Editor.Focus()` to focus the Editor, once 
clicked. It's because the pointer.Area is larger than the Editor itself, 
due to the icons and paddings e such. So, if you click on the icon: you 
also focus com the Editor. ;) 

I have alot of helper functions (gutils.*) so sharing the code is hard. :\
However, the `e.drawSliderInvisibleButton(gtx)` is basically the
pointer.Area and pointer.PassOp, as I mentioned before, so nothing
special.

That is how I did it: but I don't know if it's the best way to do. :P

-- 
  Lucas Rodrigues
  inkeliz@inkeliz.com

On Mon, Jun 21, 2021, at 4:36 PM, masala@riseup.net wrote:
> On 2021-06-21 17:01, Lucas Rodrigues wrote:
> > I have similar feature on my app. A long-press open a small dialog to
> > "copy" and "paste", into some Editor. You need to use
> > `pointer.PassOp{Pass: true}.Add(gtx.Ops)`.
> > 
> > I think you should do something like:
> > 
> > paste := NewLongPress(a.w.Invalidate, 800*time.Millisecond)
> > 
> > dims := material.Editor(th, c.compose, "").Layout(gtx)
> > 
> > state := op.Save(gtx.Ops)
> > pointer.PassOp{Pass: true}.Add(gtx.Ops) // << Here
> > pointer.Rect(image.Rectangle{Max: dims.Size}).Add(gtx.Ops)
> > paste.Add(gtx.Ops)
> > state.Load()
> > 
> > You use the `pointer.PassOp` to not block the click, so the Editor can
> > receive them too. The `Stack/State` is needed to prevent "leak" the
> > pointer.Rect. I think it will be enough.
> > 
> > -- 
> >   Lucas Rodrigues
> >   inkeliz@inkeliz.com
> 
> Ok! That worked. I am a bit confused but I think the main difference is
> that
> the PassOp is being applied to the AreaOp - so that the events then
> reach the
> material.Editor's "hit area" - ie:
> 
> ...
> (stack growing downlwards)
> | <- stack saved
> PassOp
> |
> AreaOp <- from "LongPress"
> |
> InputOp
> | <- stack restored
> AreaOp  <- from widget.Editor
> |
> InputOp
> ...
> 
> (Ok, probably a bad rendering...)
> 
> I am afraid I dont have a good visualization of how the op.Save()/Load()
> is working, or why this is necessary, though. Time to read more I guess
> :)
> 
> p.s. src for the project is:
> https://github.com/katzenpost/catchat/tree/wip_gio_interface
> 
> if your project is also FOSS please share - I am curious how you wakeup
> upon a timer fire or otherwise detect specific intervals, I would
> ideally
> like to not have to pass a pointer to the window.Invalidate method.
> 
> Thanks again
> -M
> 

Re: ***SPAM*** Re: pass-through pointer.Press events, or, how to overlay pointer handlers

Details
Message ID
<e9570cb32fdcb288d212ec33dcb8ca1e@riseup.net>
In-Reply-To
<81a6911e-96c9-40aa-aa49-a38c99815dbf@www.fastmail.com> (view parent)
DKIM signature
pass
Download raw message
On 2021-06-21 20:29, Lucas Rodrigues wrote:
> Unfurtunelly, it's not open source, but I can share some code. 
> 
> What happens is that your pointer.Area is overlaying the Editor, so 
> imagine like a Z-Index: 
> 
> Z-Index 1: Editor 
> Z-Index 2: LongPress (Your custom pointer.Area) 
> 
> By default, only one pointer.Area receives the events, which is the last 
> one. The pointer.PassOp allows passing the events to the 
> next one, in that case: "Z-Index 1". Now, both can receive events.

Ok that makes sense. I had some idea that the pointer.Events were
delivered to the input handlers but didn't realize there was a hierarchy
of the "hit" areas first.
> 
> -------------
> 
> You don't need to use `window.Invalidate`, you should use 
> `op.InvalidateOp{}.Add(gtx.Ops)` instead. 
> 
> However, you may need to set the "At" on `op.InvalidateOp{}`, so it will 
> "wake at some point in the future". 

Oh, this is good to know. Thanks!

> 
> If you call: `op.InvalidateOp{}.Add(gtx.Ops)` it will generate a new 
> frame, when the current frame is done. It's good for animations. In the 
> end: it will generate a new frame, each 16ms, if you set that op each 
> frame. 
> 
> If you call `op.InvalidateOp{At: time.Now().Add(300*time.Millisecond 
> )}.Add(gtx.Ops)` it will wait 300 milliseconds, and then Gio will 
> generate a new frame. That might be what you want. Since you need to 
> check if the user keeps pressing after some amount of time. 
> 
> The window.Invalidate should be used when it happens outside of the 
> frame, such as a networking event (receive a response from 
> http.Get("..."), then you call window.Invalidate). 

Yep that's basically where I am used to using it.
> 
> --------- 
> 
> That is what I use (it is since the my custom element.Input): 
> 
> ```
> for _, ev := range gtx.Queue.Events(&e.slider) {
> 	evt, ok := ev.(pointer.Event)
> 	if !ok {
> 		continue
> 	}
> 
> 	switch evt.Source {
> 	case pointer.Mouse:
> 		switch {
> 		case evt.Type == pointer.Press && evt.Buttons == pointer.ButtonPrimary:
> 			if !e.Editor.Focused() {
> 				e.Editor.Focus()
> 			}
> 		case evt.Type == pointer.Press && evt.Buttons ==
> pointer.ButtonSecondary: // Right Click
> 			if !e.Editor.Focused() {
> 				e.Editor.Focus()
> 			}
> 			e.slider.Toggle()
> 		}
> 	case pointer.Touch:
> 		switch evt.Type {
> 		case pointer.Press:
> 			if !e.Editor.Focused() {
> 				e.Editor.Focus()
> 			}
> 			e.sliderMobilePress = gtx.Now  
> 		case pointer.Release:
> 			e.sliderMobilePress = time.Time{}
> 		case pointer.Cancel:
> 			e.sliderMobilePress = time.Time{}
> 		}
> 	}
> }
> 
> if e.sliderMobilePress != (time.Time{}) {
> 	if gtx.Now.Sub(e.sliderMobilePress) >= 300*time.Millisecond {
> 		if !e.Editor.Focused() {
> 			e.Editor.Focus()
> 		}
> 		e.slider.Toggle()
> 		e.sliderRequested = true
> 		e.sliderMobilePress = time.Time{}
> 	} else {
> 		// gutils.Vibrate(gtx.Ops)
> 		gutils.InvalidateAfter(gtx.Ops, 300*time.Millisecond)
> 	}
> }
> ```
> 
> The ` gutils.InvalidateAfter` calls the `op.InvalidateOp{}`, the ` 
> e.slider` is a custom `elements.Slider` which is used to display the 
> "Copy" and "Paste". The `e.sliderMobilePress` is a time.Time, so
> it check when the user clicks, if it's larger than 300 miliseconds
> it will open. The e.sliderMobilePress is reseted to `time.Time{}`
> when the user releases the button, or open the slider.

Ah, I didn't try to do any animations/slick looking stuff yet - 
> 
> The layout is rendered as: 
> 
> gutils.Stack(gtx, func(gtx layout.Context) { 
> call.Add(gtx.Ops) // << Editor 
> }) 
> 
> e.drawBorder(gtx) // << Borders 
> e.drawCover(gtx) // << Icons 
> e.drawSlider(gtx) // << Copy/Paste button slider 
> e.drawSliderInvisibleButton(gtx) // << The pointer.Area (...) that gets 
> the clicks to open the slider, it uses `op.PassOp`. ;) 
> ```
> 
> If you notice, my code uses `e.Editor.Focus()` to focus the Editor, once 
> clicked. It's because the pointer.Area is larger than the Editor itself, 
> due to the icons and paddings e such. So, if you click on the icon: you 
> also focus com the Editor. ;) 

Aha, yeah, I also needed to use Focus to ensures the editor receives
focus afterwards.
> 
> I have alot of helper functions (gutils.*) so sharing the code is hard. :\

Hahah well I know the code I wrote so far is a real good example of...
"showing the full horror of what I have done"
and should really refactor/clean it up so these huge Layout functions
aren't multi-page affairs :)
but since it works at the moment...

> However, the `e.drawSliderInvisibleButton(gtx)` is basically the
> pointer.Area and pointer.PassOp, as I mentioned before, so nothing
> special.
> 
> That is how I did it: but I don't know if it's the best way to do. :P

I'm having a lot of fun with gio but it does seem to require a lot of
reading the source to understand what is actually going on. I'm not
really familiar with much in the UX/graphics though so basically
everything is like that to me, though.

Thanks a lot for the tips!
-M
> 
> -- 
>   Lucas Rodrigues
>   inkeliz@inkeliz.com
> 
> On Mon, Jun 21, 2021, at 4:36 PM, masala@riseup.net wrote:
>> On 2021-06-21 17:01, Lucas Rodrigues wrote:
>> > I have similar feature on my app. A long-press open a small dialog to
>> > "copy" and "paste", into some Editor. You need to use
>> > `pointer.PassOp{Pass: true}.Add(gtx.Ops)`.
>> >
>> > I think you should do something like:
>> >
>> > paste := NewLongPress(a.w.Invalidate, 800*time.Millisecond)
>> >
>> > dims := material.Editor(th, c.compose, "").Layout(gtx)
>> >
>> > state := op.Save(gtx.Ops)
>> > pointer.PassOp{Pass: true}.Add(gtx.Ops) // << Here
>> > pointer.Rect(image.Rectangle{Max: dims.Size}).Add(gtx.Ops)
>> > paste.Add(gtx.Ops)
>> > state.Load()
>> >
>> > You use the `pointer.PassOp` to not block the click, so the Editor can
>> > receive them too. The `Stack/State` is needed to prevent "leak" the
>> > pointer.Rect. I think it will be enough.
>> >
>> > --
>> >   Lucas Rodrigues
>> >   inkeliz@inkeliz.com
>>
>> Ok! That worked. I am a bit confused but I think the main difference is
>> that
>> the PassOp is being applied to the AreaOp - so that the events then
>> reach the
>> material.Editor's "hit area" - ie:
>>
>> ...
>> (stack growing downlwards)
>> | <- stack saved
>> PassOp
>> |
>> AreaOp <- from "LongPress"
>> |
>> InputOp
>> | <- stack restored
>> AreaOp  <- from widget.Editor
>> |
>> InputOp
>> ...
>>
>> (Ok, probably a bad rendering...)
>>
>> I am afraid I dont have a good visualization of how the op.Save()/Load()
>> is working, or why this is necessary, though. Time to read more I guess
>> :)
>>
>> p.s. src for the project is:
>> https://github.com/katzenpost/catchat/tree/wip_gio_interface
>>
>> if your project is also FOSS please share - I am curious how you wakeup
>> upon a timer fire or otherwise detect specific intervals, I would
>> ideally
>> like to not have to pass a pointer to the window.Invalidate method.
>>
>> Thanks again
>> -M
>>
Reply to thread Export thread (mbox)