81 Commits

Author SHA1 Message Date
Elias Naur f200f0e9a3 app: [Windows] don't draw minimized windows
Fixes: https://todo.sr.ht/~eliasnaur/gio/621
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2025-01-14 11:29:45 +01:00
Elias Naur 1ae2b9b8fe app: [macOS] don't draw when minimized
References: https://todo.sr.ht/~eliasnaur/gio/621
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2025-01-14 11:29:45 +01:00
Elias Naur fe4bf00c70 app: [macOS] propagate unhandled key shortcuts
By propagation, we restore the system behaviour for shortcuts the program
don't want, for example the system beep.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2025-01-14 11:12:34 +01:00
Elias Naur 8107ec2206 app: [macOS] remove support for key bindings
The fix for #616 went to far by attempting to support macOS key bindings
through doCommandBySelector. Issue #625 is a consequence, but more
fundamentally, key bindings does not work with support for key.Release
events.

Remove key binding translation and fix #626, and add a check whether an
IME swallowed a key event, keeping #616 fixed.

While here, replace the KEY_ constants with ASCII codes.

Fixes: https://todo.sr.ht/~eliasnaur/gio/625
References: https://todo.sr.ht/~eliasnaur/gio/616
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2025-01-13 23:24:25 +01:00
Chris Waldon d2db4f6875 internal/{egl,gl}: [Windows] restrict graphics DLL sources
In order to avoid DLL preloading attacks, we should be careful about where we
load DLLs from. These packages load graphics DLLs, which may be provided by the
OS, by a graphics vendor, or even by individual applications. As such, we can't
restrict loading them to just system32-provided paths. Instead, we invoke
LoadLibraryEx [0] with the LOAD_LIBRARY_SEARCH_DEFAULT_DIRS path, which will search
system32, application-defined paths, and the path of the primary application
executable. This mode ignores the system %PATH% variable, which dramatically
reduces the attack surface of malicious or unintended DLLs.

Applications may add custom paths to the search list by calling the standard
windows AddDllDirectory function [1] prior to attempting to initialize GL.

Thanks to Mohsen Mirzakhani and Utkarsh Satya Prakash for bringing this to
our attention.

[0] https://learn.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibraryexa
[1] https://learn.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-adddlldirectory

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2025-01-09 21:57:47 +01:00
Elias Naur 971f86ea7e .builds: work around iOS build failure
Later versions of clang no longer accepts our ancient SDK root. Force it
by supressing a warning.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2025-01-09 11:25:27 +01:00
Elias Naur e1fbb189e5 .builds: upgrade FreeBSD builder
It's currently failing, and upgrading it may fix it.

While here, track lates FreeBSD to avoid upgrade toil.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2025-01-09 10:05:03 +01:00
Chris Waldon a206e5e847 app: [Windows] use NewLazySystemDLL for kernel32.dll
In order to avoid DLL preloading attacks, we should always load our system
dependencies using the helper that only searches the system library path.

Thanks to Mohsen Mirzakhani and Utkarsh Satya Prakash for bringing this to
our attention.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2025-01-09 09:54:15 +01:00
Chris Waldon e025ed1344 widget: ensure editor does not trim trailing whitespace
This commit makes the editor widget suppress the trimming of trailing whitespace
so that the spaces can be selected intuitively.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2024-12-18 13:28:16 -05:00
Chris Waldon b576252963 text: allow disabling space trimming
This commit adds a shaping parameter that disables the trimming of trailing
whitespace from lines. Text editors and similar use-cases want trailing whitspace
glyphs to be selectable, which means they must occupy space.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2024-12-18 13:28:16 -05:00
Chris Waldon 1d0d5f0383 widget: update text indexing expectations
The typesetting package has smarter line wrapping now, which is making our
test text require fewer lines to display. We needed to update the expected
data accordingly.

I've also added a feature that takes a screenshot of the rendered output of
one of our most complex cursoring tests. This will make it much easier to
verify its behavior in the future. This feature currently only triggers if
the test case fails.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2024-12-18 13:28:16 -05:00
Chris Waldon 26cddb00b1 text: use upstream bidi visual order algorithm
We've migrated the processing of bidi run ordering into the upstream typesetting
package, so now we can just consume the already-ordered runs instead of computing
their ordering ourselves.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2024-12-18 13:28:16 -05:00
Benoit KUGLER 0cbbacc45a [text, font] Bump go-text version to 0.2.1
Signed-off-by: Benoit KUGLER <benoit.kugler@gmail.com>
2024-12-18 13:28:10 -05:00
Elias Naur 1d95c7c6b3 flake.*: upgrade to nixpkgs 24.11
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-12-06 10:23:02 +01:00
Elias Naur 7337c06daf app: [macOS] send keypress events for modifier keys
This change generates keypress and release events for modifier keys in macOS.
Specifically the Control, Alt, Shift and Command keys.

Signed-off-by: Jeff Williams <kanobe@gmail.com>
2024-12-04 18:12:15 +01:00
Elias Naur 94355e5201 internal/vk: remove methods on C types, for Go 1.24 compatibility
Go 1.24 no longer allows methods on C types (golang.org/issue/60725).

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-11-26 10:31:04 +01:00
Elias Naur ea456f42c7 gpu: remove compute renderer
The compute renderer is a failed experiment: a better port of the
Vello vector renderer exists[0] and the upcoming Go 1.24 release
no longer builds the gioui.org/cpu module because of #60725.

Remove it.

[0] https://github.com/dominikh/jello

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-11-26 08:59:36 +00:00
Elias Naur 8daff13af6 app: [windows] ensure no callbacks after DestroyEvent
Setting the callback handler to nil in DestroyEvent should have no effect,
but may help debugging #603.

Also don't call the default window handler for WM_DESTROY since we're
already handling it.

References: https://todo.sr.ht/~eliasnaur/gio/603
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-10-25 22:06:57 +02:00
Elias Naur aa158e0c9c app: fix race condition between Window.Invalidate and event loop
References: https://todo.sr.ht/~eliasnaur/gio/603
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-10-25 20:38:37 +02:00
Elias Naur 520efdfa75 app: fix typo
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-10-25 20:29:21 +02:00
Elias Naur d7a1ec7461 app: [macos] don't relay key events handled by the IME
Widgets such as Editor use certain key events such as the backspace key
to implement text editing. On macOS, such key events are sometimes used
by an input method, and in those cases the key effect would be applied
twice: first by the IME and then the Editor.

Report such key events through the doCommandBySelector callback, which
receives key events not handled by the IME.

References: https://todo.sr.ht/~eliasnaur/gio/616
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-10-25 19:39:20 +02:00
Elias Naur d4c5e54375 app: remove dead code
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-10-25 19:39:20 +02:00
Elias Naur 44ac50506d app: [android] ensure a new frame is always scheduled when visible and animating
Possible fix to #614.

References: https://todo.sr.ht/~eliasnaur/gio/614
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-10-25 12:16:54 +02:00
inkeliz 38e4b1c6a4 app: [android] fix compatibility with older Android versions
Previously, setHighRefreshRate requires APIs restricted
to Android 30, or higher.

Tested on Android 6.0.1 (released on 2015).

Signed-off-by: inkeliz <inkeliz@inkeliz.com>
2024-10-25 11:44:13 +02:00
Chris Waldon 5d886b4d7f go.*: update typesetting for unifont panic fix
This commit updates our typesetting dependency to avoid a crash when shaping GNU
unifont. Thanks to Jeff Williams for raising the issue on the mailing list.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2024-10-22 19:05:27 +02:00
Elias Naur c7277581f8 app: [Window] don't report Win32ViewEvent before window configuration
Fixes: https://todo.sr.ht/~eliasnaur/gio/609
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-10-22 19:05:27 +02:00
Elias Naur a5f7e7b2c7 app: [Windows] compute min, max window sizes correctly when un-minimzing
Fixes: https://todo.sr.ht/~eliasnaur/gio/608
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-10-22 19:05:27 +02:00
Elias Naur 0781e62b56 app: [Windows] don't ignore Min|MaxSize options
The support for minimum and maximum window sizes were broken by a recent
change. Found while investigating #608.

References: https://todo.sr.ht/~eliasnaur/gio/608
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-10-22 19:05:27 +02:00
Larry Clapp 95f63c66b6 text,widget/material: Update doc for Shaper & Theme
Note that you should use different Themes, with different Shapers, for
different top-level windows, and explain why.

Signed-off-by: Larry Clapp <larry@theclapp.org>
2024-10-22 19:05:27 +02:00
Elias Naur 6efcb65c4b Revert "app: [Windows] don't draw after WM_DESTROY destroyed the window"
This reverts commit 635df374952019ff8d274646ea9ce040744daa0f because it
didn't fix #603 after all.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-10-22 19:05:27 +02:00
Elias Naur f6e9f6861d app: close the window before reporting a GPU error
When a GPU error occurs forcing the reporting of a DestroyEvent is not
appropriate, because the backend that controls the underlying window
is not aware of the error and will continue to report events.

Replace the crude DestroyEvent by stashing the error and asking the
window nicely to close. The, report the stashed error in the
otherwise regular DestroyEvent.

Hopefully, this second attempt fixes #603.

Fixes: https://todo.sr.ht/~eliasnaur/gio/603
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-10-22 19:05:27 +02:00
Elias Naur 6c6cc157c4 layout,io/input: [API] change Context.Disabled to only disable events
Also unexport Source.Enabled because the nil-ness of its embedded router
is now an implementation detail.

Fixes: https://todo.sr.ht/~eliasnaur/gio/605
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-10-22 19:05:27 +02:00
Elias Naur 8cf449034c app: [Windows] remove redundant code
Change f7aa4b5c8 changed the fullscreen implementation to no longer require
the position and size of the fullscreen window.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-10-22 19:05:27 +02:00
Elias Naur 6722c7960a widget: [API] change Decorations to leave the user in control of window state
As suggested by ~egonelbre, Decorations should not be the source of truth
for the windows state, because external gestures may also change state.

This breaking change removes Decorations.Perform and exposes Maximized
as a bool which is the user's responsibility to set.

Fixes: https://todo.sr.ht/~eliasnaur/gio/600
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-10-22 19:05:27 +02:00
Elias Naur 97044e53b5 app: [Windows] track window state changes initiated by the OS
Like the previous change, update the Windows backend to track and report
window state changes initiated by the OS.

References: https://todo.sr.ht/~eliasnaur/gio/600
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-10-22 19:05:27 +02:00
Elias Naur af6dda67a5 app: [macOS] track window state changes initiated by the operating system
Before this change, the window state was explicitly updated whenever
Window.Option was called. However, the system may also change window
state as a result of user gestures, but those changes did not result in
ConfigEvents reflecting them.

Remove the explicit state updates and track them when the system tells
us it has changed.

This is a step towards fixing #600 which require accurate window state
tracking.

References: https://todo.sr.ht/~eliasnaur/gio/600
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-10-22 19:05:27 +02:00
Elias Naur 042ed4ab49 app: [Windows] don't draw after WM_DESTROY destroyed the window
There may be a window of time from WM_DESTROY is received to the WM_QUIT
message is delivered by PostQuitMessage. If so, we must not call w.draw.

Fixes: https://todo.sr.ht/~eliasnaur/gio/603
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-10-22 19:05:27 +02:00
Elias Naur 6aa027136e app: [Windows] remove redundant code
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-10-22 19:05:27 +02:00
Elias Naur b1db32ef72 io/input: fix typo
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-10-22 19:05:27 +02:00
Elias Naur 9dceca6c95 Revert "app: [Windows] suppress double-click behaviour for custom decorations"
This reverts commit 82cbb7b8da.

As Egon Elbre points out in

https://todo.sr.ht/~eliasnaur/gio/600#event-377179

this is not the right fix.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-10-22 19:05:19 +02:00
Egon Elbre 7293fa8a41 go.mod: update golang.org/x dependencies
This gets rid of a CVE warning about golang.org/x/image/tiff.

Signed-off-by: Egon Elbre <egonelbre@gmail.com>
2024-07-16 15:59:05 +02:00
Elias Naur 82cbb7b8da app: [Windows] suppress double-click behaviour for custom decorations
Fixes: https://todo.sr.ht/~eliasnaur/gio/600
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-07-13 18:09:13 +02:00
Chris Waldon 6c19821a6c app: [Wayland] prevent recursive scroll event processing
This commit zeroes the accumulated scroll distance on the window before invoking the
event delivery code, since the event delivery code is able to call back into the scroll
processing. Prior to this change, the callback could re-processing the scroll delta
while magnifying it by a factor of 10.

Fixes: https://todo.sr.ht/~eliasnaur/gio/599
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2024-07-13 11:03:14 +02:00
Elias Naur 86349775b7 app: ensure Invalidate can be invoked when window is closing
This commit ensures that it is safe to invoke Invalidate() from another goroutine
while a Gio window may be in the process of closing. It can be difficult to prevent
this from happening, as window handles can easily be managed by a type that doesn't
know the exact moment of window close (it might be waiting on the window event loop
to return, but that hasn't happened yet). Without this change, the nil window
driver results in a panic in this situation.

Co-authored-by: Chris Waldon <christopher.waldon.dev@gmail.com>
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-07-02 15:06:52 +02:00
Chris Waldon 4a1b4c2642 app: add cross-platform empty view event detection
Custom rendering applications need to be prepared to handle empty view events,
as an empty view event is sent during window shutdown. However, the current
implementation requires applications to write a platform-specific helper
function for each supported platform in order to check whether a received
view event is empty. This commit provides a safe, convenient, cross-platform
method that applications can use to detect this special view event and respond
to it.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2024-06-28 08:46:36 +02:00
Elias Naur c900d58fb3 app: [macOS] fix ANGLE renderering
Setting CAMetalLayer.presentWithTransaction to YES breaks ANGLE rendering.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-06-27 17:48:08 +02:00
Elias Naur 74ccc9c2c7 app: use empty frame when FrameEvent.Frame isn't called
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-06-27 16:37:48 +02:00
Elias Naur 3f671afea8 app: ignore Invalidate for Windows not yet created
While here, don't overflow the Windows event queue.

Fixes: https://todo.sr.ht/~eliasnaur/gio/596
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-06-27 16:37:48 +02:00
Elias Naur 42357a29e0 app: reset Window when DestroyEvent is received
Fixes: https://todo.sr.ht/~eliasnaur/gio/595
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-06-27 10:41:22 +02:00
Elias Naur 8fb6d3da2b io/input: deliver all observed events before deferring the rest
Even when a command defers event delivery to the next frame, the already
observed events must still be delivered in the current frame. This
matters for pointer events that hit more than one event handler.

Fixes: https://todo.sr.ht/~eliasnaur/gio/594
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-06-20 15:26:33 +02:00
Elias Naur 706940ff9b io/input: improve documentation, code
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-06-20 13:23:36 +02:00
Chris Waldon 5542aac772 app: queue system actions before first call to Event()
This commit ensures that attempting to perform a system window action prior
to the first call to Event() does not panic. It adopts a similar strategy to
handling Option() prior to the first call to Event(): make a slice of the arguments
and apply them during window initialization.

Fixes: https://todo.sr.ht/~eliasnaur/gio/593
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2024-06-20 12:18:42 +02:00
Elias Naur 026d3f9daa .builds: increase file descriptor limit for Android's sdkmanager
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-06-20 09:53:17 +02:00
Elias Naur 38fca9ae13 widget: show software keyboard when a writable Editor is clicked
Extracted from https://github.com/gioui/gio/pull/138 by Inkeliz.

References: https://todo.sr.ht/~eliasnaur/gio/591
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-06-16 15:20:49 +02:00
Elias Naur e878dbc598 app: [macOS] ignore focus changes not meant for the Gio view
Extracted from https://github.com/gioui/gio/pull/138 by inkeliz.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-06-16 15:15:40 +02:00
inkeliz 1151eac07d pointer: fix documentation
Previously it uses event.Op{}, but such struct don't
exists anymore. Instead, it have a function with the
same name.

Signed-off-by: inkeliz <inkeliz@inkeliz.com>
2024-06-07 10:35:07 +02:00
Elias Naur 56177c55cf io/input: remove unused field
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-06-07 10:32:44 +02:00
Elias Naur e6da07a85a app: [iOS] add support for buildmode exe
Up until now, the iOS part has relied on a tool such as gogio to
synthesize a main function. This change adds support for running direcetly
in exe mode, while retaining support for embedded Gio in C programs.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-06-02 12:55:36 +02:00
Elias Naur 175e134478 app: [macOS] panic if Main is not called from the main goroutine
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-06-02 09:07:29 +02:00
Elias Naur 46cc311d19 app: fix typos
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-05-30 10:04:36 +02:00
Elias Naur b8821875ed Revert "app: [macOS] ensure the Window is initalized before Run functions"
This reverts commit 5083a23301 because Option
and Run no longer create the window.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-05-30 10:03:37 +02:00
Elias Naur f6e33914d9 Revert "app: [Windows] ensure the Window is initalized before Run functions"
This reverts commit 971b01d836 because Option
and Run no longer creates the window.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-05-30 10:03:37 +02:00
Elias Naur a394b330e8 app: defer window creation until Window.Event is called
We're moving towards making Window.Event, and in the future, Window.Events
create the window and drive the event loop to completion. In that model,
the other Window methods shouldn't create the window.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-05-30 10:03:32 +02:00
Jack Mordaunt 24b0c2a4a1 internal/gl: [Windows] allow GetProgramInfoLog to return nothing
If GetProgrami returns 0 we will panic because a zero-sized buffer indexed
at zero will OOB panic: "runtime error: index out of range [0] with length 0".

This was observed and is not theoretical.

Windows 8 9200

Hardware: {
  "board": {
    "name": "1963",
    "vendor": "Hewlett-Packard"
  },
  "cpu": {
    "name": "Intel(R) Core(TM) i7-4700MQ CPU @ 2.40GHz",
    "vendor": "GenuineIntel",
    "extra": {
      "cores": "4",
      "threads": "8"
    }
  },
  "ram": "8.00GB"
}

panic({0x7f7222ff260?, 0xc004b960d8?})
	runtime/panic.go:770 +0x132
gioui.org/internal/gl.(*Functions).GetProgramInfoLog(0xc004cd4000?, {0x7f720039345?})
	gioui.org@v0.4.2-0.20231216201919-2128f7adea9b/internal/gl/gl_windows.go:365 +0xc5
gioui.org/gpu/internal/opengl.(*Backend).newProgram(0xc004c8e008, {{0x7f7229d0b40, 0xc004e3a000}, {0x7f7229d0b60, 0xc004e3a120}, {{0xc002b04060, 0x2, 0x2}, 0x10}, {0x1, ...}, ...})
	gioui.org@v0.4.2-0.20231216201919-2128f7adea9b/gpu/internal/opengl/opengl.go:954 +0x26b
gioui.org/gpu/internal/opengl.(*Backend).NewPipeline(0x2ec?, {{0x7f7229d0b40, 0xc004e3a000}, {0x7f7229d0b60, 0xc004e3a120}, {{0xc002b04060, 0x2, 0x2}, 0x10}, {0x1, ...}, ...})
	gioui.org@v0.4.2-0.20231216201919-2128f7adea9b/gpu/internal/opengl/opengl.go:918 +0x65
gioui.org/gpu.createColorPrograms({_, _}, {{0x7f72241fcbb, 0x9}, {0x0, 0x0}, {0x7f72259c680, 0x42a}, {0x0, 0x0}, ...}, ...)
	gioui.org@v0.4.2-0.20231216201919-2128f7adea9b/gpu/gpu.go:601 +0x332
gioui.org/gpu.newBlitter({0x7f7229fea48, 0xc004c8e008})
	gioui.org@v0.4.2-0.20231216201919-2128f7adea9b/gpu/gpu.go:559 +0x358
gioui.org/gpu.newRenderer({0x7f7229fea48, 0xc004c8e008})
	gioui.org@v0.4.2-0.20231216201919-2128f7adea9b/gpu/gpu.go:516 +0x25
gioui.org/gpu.(*gpu).init(...)
	gioui.org@v0.4.2-0.20231216201919-2128f7adea9b/gpu/gpu.go:373
gioui.org/gpu.newGPU({0x7f7229fea48, 0xc004c8e008})
	gioui.org@v0.4.2-0.20231216201919-2128f7adea9b/gpu/gpu.go:365 +0x14d
gioui.org/gpu.NewWithDevice({0x7f7229fea48, 0xc004c8e008})
	gioui.org@v0.4.2-0.20231216201919-2128f7adea9b/gpu/gpu.go:355 +0x10c
gioui.org/gpu.New({0x7f7229cbdc0?, 0xc004bd2000?})
	gioui.org@v0.4.2-0.20231216201919-2128f7adea9b/gpu/gpu.go:342 +0x34

Signed-off-by: Jack Mordaunt <jackmordaunt.dev@gmail.com>
2024-05-25 18:24:35 +02:00
Walter Werner SCHNEIDER 7a9ce51988 widget: add more editor shortcuts
Signed-off-by: Walter Werner SCHNEIDER <contact@schnwalter.eu>
2024-05-06 08:46:20 -04:00
Walter Werner SCHNEIDER 8242234274 internal/stroke: fix normal vector size
With this change the GPU renderer now properly handles the cases when the stroke width equals the stroke length where the normal vector is the same size as the original vector.

Fixes: https://todo.sr.ht/~eliasnaur/gio/576
Signed-off-by: Walter Werner SCHNEIDER <contact@schnwalter.eu>
2024-05-06 10:45:29 +02:00
Elias Naur 691adf4e77 app: [X11] don't recreate EGL surface during resize
According to #565 X11 GPU drivers don't deal well with recreation of
EGL surfaces.

Thanks to Walter Schneider for debugging this issue and coming up with
the original patch.

Fixes: https://todo.sr.ht/~eliasnaur/gio/565
Co-authored-by: Walter Werner SCHNEIDER <contact@schnwalter.eu>
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-05-01 19:59:26 +02:00
Elias Naur ba1e34e570 app: [X11] add missing check for destroyed window
Fixes: https://todo.sr.ht/~eliasnaur/gio/577
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-04-26 16:44:50 +02:00
owhionetrusetuhecruos@schn.email 0deb7b3efc material: improve progress indicator layout
Fixes: https://todo.sr.ht/~eliasnaur/gio/570
Signed-off-by: Walter Werner SCHNEIDER <contact@schnwalter.eu>
2024-04-21 08:51:20 +02:00
Elias Naur e8c73bcb37 app: [Wayland] suppress spurious ConfigEvents
As reported By Larry Clapp, Wayland would send a ConfigEvent with
every FrameEvent when fallback client side decorations are enabled.
This is because Window would call the driver Option and Perform
methods even when they're empty.

The change applies to every platform, but was only observable on
Wayland.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-04-20 20:25:08 +02:00
Elias Naur cf9f2bbffe app: [Wayland] don't send events after DestroyEvent
Like a previous commit for X11, this change ensures no events are
sent after DestroyEvent.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-04-20 20:25:03 +02:00
Elias Naur ed28861309 app: [X11] don't send events after DestroyEvent
Before this change, a FrameEvent may be delivered after DestroyEvent,
leading to a panic. Destroy the X11 window immediately thus ensuring no
events can be delivered after destroy.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-04-20 09:08:06 +02:00
Elias Naur 971b01d836 app: [Windows] ensure the Window is initalized before Run functions
Like the previous commit for macOS, this defers event processing until
after the Window is ready.

Fixes: https://todo.sr.ht/~eliasnaur/gio/575
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-04-19 17:19:45 +02:00
Elias Naur 5083a23301 app: [macOS] ensure the Window is initalized before Run functions
Don't call eventLoop.FlushEvents which in turn applies Options and
executes Run functions before the window is fully initialized.

References: https://todo.sr.ht/~eliasnaur/gio/575
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-04-19 17:08:14 +02:00
Elias Naur 61b603d521 .builds: bump builders to Go 1.22
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-04-15 10:21:41 +02:00
Egon Elbre 3b5148a64e op/clip: add note about Path.End
Signed-off-by: Egon Elbre <egonelbre@gmail.com>
2024-04-15 10:18:25 +02:00
Elias Naur ee6cdec60b io/pointer: [API] split scroll bounds into two separate axes
A single image.Rectangle for the scroll bounds introduced a subtle issue
with zero area rectangles (see #572). To avoid that and similar issues,
split the bounds into two separate one-dimensional ranges.

Fixes: https://todo.sr.ht/~eliasnaur/gio/572
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-04-14 08:54:12 +02:00
Elias Naur 42ef3476cc go.mod: bump minimum Go to 1.21
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-04-14 08:44:46 +02:00
Elias Naur 98d3a2eb24 gpu: fix viewport arguments for opacity layers
Fixes: https://todo.sr.ht/~eliasnaur/gio/574
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-04-12 23:34:18 +02:00
Elias Naur 109226b7e9 gpu: ensure opacity layers are rendered with correct pixel formats
FBOs and window framebuffers generally have different pixel formats, and
so require separate pipeline configurations.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-04-12 18:08:20 +02:00
Elias Naur 477bd5c744 gpu: remove unused parameter
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-04-12 17:20:23 +02:00
68 changed files with 1514 additions and 3486 deletions
+2 -2
View File
@@ -29,7 +29,7 @@ environment:
tasks:
- install_go: |
mkdir -p /home/build/sdk
curl -s https://dl.google.com/go/go1.19.11.linux-amd64.tar.gz | tar -C /home/build/sdk -xzf -
curl -s https://dl.google.com/go/go1.22.2.linux-amd64.tar.gz | tar -C /home/build/sdk -xzf -
- prepare_toolchain: |
mkdir -p $APPLE_TOOLCHAIN_ROOT
cd $APPLE_TOOLCHAIN_ROOT
@@ -71,4 +71,4 @@ tasks:
CC=$APPLE_TOOLCHAIN_ROOT/tools/clang-macos GOOS=darwin CGO_ENABLED=1 go build ./...
- test_ios: |
cd gio
CC=$APPLE_TOOLCHAIN_ROOT/tools/clang-ios GOOS=darwin GOARCH=arm64 CGO_ENABLED=1 go build -tags ios ./...
CGO_CFLAGS=-Wno-deprecated-module-dot-map CC=$APPLE_TOOLCHAIN_ROOT/tools/clang-ios GOOS=darwin GOARCH=arm64 CGO_ENABLED=1 go build -tags ios ./...
+2 -2
View File
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Unlicense OR MIT
image: freebsd/13.x
image: freebsd/latest
packages:
- libX11
- libxkbcommon
@@ -16,7 +16,7 @@ environment:
tasks:
- install_go: |
mkdir -p /home/build/sdk
curl https://dl.google.com/go/go1.19.11.freebsd-amd64.tar.gz | tar -C /home/build/sdk -xzf -
curl https://dl.google.com/go/go1.22.2.freebsd-amd64.tar.gz | tar -C /home/build/sdk -xzf -
- test_gio: |
cd gio
go test ./...
+3 -1
View File
@@ -40,7 +40,7 @@ secrets:
tasks:
- install_go: |
mkdir -p /home/build/sdk
curl -s https://dl.google.com/go/go1.19.11.linux-amd64.tar.gz | tar -C /home/build/sdk -xzf -
curl -s https://dl.google.com/go/go1.22.2.linux-amd64.tar.gz | tar -C /home/build/sdk -xzf -
- check_gofmt: |
cd gio
test -z "$(gofmt -s -l .)"
@@ -80,6 +80,8 @@ tasks:
unzip -q ndk.zip
rm ndk.zip
mv android-ndk-* ndk-bundle
# sdkmanager needs lots of file descriptors
ulimit -n 10000
yes|sdkmanager --licenses
sdkmanager "platforms;android-31" "build-tools;32.0.0"
- test_android: |
+1 -1
View File
@@ -10,7 +10,7 @@ environment:
tasks:
- install_go: |
mkdir -p /home/build/sdk
curl https://dl.google.com/go/go1.19.11.src.tar.gz | tar -C /home/build/sdk -xzf -
curl https://dl.google.com/go/go1.22.2.src.tar.gz | tar -C /home/build/sdk -xzf -
cd /home/build/sdk/go/src
./make.bash
- test_gio: |
+4
View File
@@ -259,6 +259,10 @@ public final class GioView extends SurfaceView implements Choreographer.FrameCal
}
private void setHighRefreshRate() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
return;
}
Context context = getContext();
Display display = context.getDisplay();
Display.Mode[] supportedModes = display.getSupportedModes();
+4
View File
@@ -61,6 +61,10 @@ type FrameEvent struct {
type ViewEvent interface {
implementsViewEvent()
ImplementsEvent()
// Valid will return true when the ViewEvent does contains valid handles.
// If a window receives an invalid ViewEvent, it should deinitialize any
// state referring to handles from a previous ViewEvent.
Valid() bool
}
// Insets is the space taken up by
+4 -6
View File
@@ -17,9 +17,8 @@ import (
)
type androidContext struct {
win *window
eglSurf egl.NativeWindowType
width, height int
win *window
eglSurf egl.NativeWindowType
*egl.Context
}
@@ -45,9 +44,8 @@ func (c *androidContext) Refresh() error {
if err := c.win.setVisual(c.Context.VisualID()); err != nil {
return err
}
win, width, height := c.win.nativeWindow()
win, _, _ := c.win.nativeWindow()
c.eglSurf = egl.NativeWindowType(unsafe.Pointer(win))
c.width, c.height = width, height
return nil
}
@@ -55,7 +53,7 @@ func (c *androidContext) Lock() error {
// The Android emulator creates a broken surface if it is not
// created on the same thread as the context is made current.
if c.eglSurf != nil {
if err := c.Context.CreateSurface(c.eglSurf, c.width, c.height); err != nil {
if err := c.Context.CreateSurface(c.eglSurf); err != nil {
return err
}
c.eglSurf = nil
+1 -1
View File
@@ -69,7 +69,7 @@ func (c *wlContext) Refresh() error {
}
c.eglWin = eglWin
eglSurf := egl.NativeWindowType(uintptr(unsafe.Pointer(eglWin)))
if err := c.Context.CreateSurface(eglSurf, width, height); err != nil {
if err := c.Context.CreateSurface(eglSurf); err != nil {
return err
}
if err := c.Context.MakeCurrent(); err != nil {
+12 -17
View File
@@ -5,8 +5,6 @@
package app
import (
"golang.org/x/sys/windows"
"gioui.org/internal/egl"
)
@@ -24,6 +22,18 @@ func init() {
if err != nil {
return nil, err
}
win, _, _ := w.HWND()
eglSurf := egl.NativeWindowType(win)
if err := ctx.CreateSurface(eglSurf); err != nil {
ctx.Release()
return nil, err
}
if err := ctx.MakeCurrent(); err != nil {
ctx.Release()
return nil, err
}
defer ctx.ReleaseCurrent()
ctx.EnableVSync(true)
return &glContext{win: w, Context: ctx}, nil
},
})
@@ -37,21 +47,6 @@ func (c *glContext) Release() {
}
func (c *glContext) Refresh() error {
c.Context.ReleaseSurface()
var (
win windows.Handle
width, height int
)
win, width, height = c.win.HWND()
eglSurf := egl.NativeWindowType(win)
if err := c.Context.CreateSurface(eglSurf, width, height); err != nil {
return err
}
if err := c.Context.MakeCurrent(); err != nil {
return err
}
c.Context.EnableVSync(true)
c.Context.ReleaseCurrent()
return nil
}
+12 -11
View File
@@ -25,6 +25,18 @@ func init() {
if err != nil {
return nil, err
}
win, _, _ := w.window()
eglSurf := egl.NativeWindowType(uintptr(win))
if err := ctx.CreateSurface(eglSurf); err != nil {
ctx.Release()
return nil, err
}
if err := ctx.MakeCurrent(); err != nil {
ctx.Release()
return nil, err
}
defer ctx.ReleaseCurrent()
ctx.EnableVSync(true)
return &x11Context{win: w, Context: ctx}, nil
}
}
@@ -37,17 +49,6 @@ func (c *x11Context) Release() {
}
func (c *x11Context) Refresh() error {
c.Context.ReleaseSurface()
win, width, height := c.win.window()
eglSurf := egl.NativeWindowType(uintptr(win))
if err := c.Context.CreateSurface(eglSurf, width, height); err != nil {
return err
}
if err := c.Context.MakeCurrent(); err != nil {
return err
}
defer c.Context.ReleaseCurrent()
c.Context.EnableVSync(true)
return nil
}
+1 -1
View File
@@ -7,7 +7,7 @@
#include <OpenGL/OpenGL.h>
#include "_cgo_export.h"
CALayer *gio_layerFactory(void) {
CALayer *gio_layerFactory(BOOL presentWithTrans) {
@autoreleasepool {
return [CALayer layer];
}
+1
View File
@@ -274,6 +274,7 @@ const (
WM_SETFOCUS = 0x0007
WM_SHOWWINDOW = 0x0018
WM_SIZE = 0x0005
WM_STYLECHANGED = 0x007D
WM_SYSKEYDOWN = 0x0104
WM_SYSKEYUP = 0x0105
WM_RBUTTONDOWN = 0x0204
+3 -2
View File
@@ -4,14 +4,15 @@ package app
import (
"log"
"syscall"
"unsafe"
syscall "golang.org/x/sys/windows"
)
type logger struct{}
var (
kernel32 = syscall.NewLazyDLL("kernel32")
kernel32 = syscall.NewLazySystemDLL("kernel32")
outputDebugStringW = kernel32.NewProc("OutputDebugStringW")
debugView *logger
)
+2 -2
View File
@@ -12,12 +12,12 @@ package app
#import <QuartzCore/CAMetalLayer.h>
#include <CoreFoundation/CoreFoundation.h>
CALayer *gio_layerFactory(void) {
CALayer *gio_layerFactory(BOOL presentWithTrans) {
@autoreleasepool {
CAMetalLayer *l = [CAMetalLayer layer];
l.autoresizingMask = kCALayerHeightSizable|kCALayerWidthSizable;
l.needsDisplayOnBoundsChange = YES;
l.presentsWithTransaction = YES;
l.presentsWithTransaction = presentWithTrans;
return l;
}
}
+4 -12
View File
@@ -173,19 +173,13 @@ type context interface {
Unlock()
}
// basicDriver is the subset of [driver] that may be called even after
// a window is destroyed.
type basicDriver interface {
// Event blocks until an even is available and returns it.
Event() event.Event
// Invalidate requests a FrameEvent.
Invalidate()
}
// driver is the interface for the platform implementation
// of a window.
type driver interface {
basicDriver
// Event blocks until an event is available and returns it.
Event() event.Event
// Invalidate requests a FrameEvent.
Invalidate()
// SetAnimating sets the animation flag. When the window is animating,
// FrameEvents are delivered as fast as the display can handle them.
SetAnimating(anim bool)
@@ -201,8 +195,6 @@ type driver interface {
Configure([]Option)
// SetCursor updates the current cursor to name.
SetCursor(cursor pointer.Cursor)
// Wakeup wakes up the event loop and sends a WakeupEvent.
// Wakeup()
// Perform actions on the window.
Perform(system.Action)
// EditorStateChanged notifies the driver that the editor state changed.
+7 -4
View File
@@ -575,10 +575,7 @@ func Java_org_gioui_GioView_onFrameCallback(env *C.JNIEnv, class C.jclass, view
if !exist {
return
}
if w.visible && w.animating {
w.draw(env, false)
callVoidMethod(env, w.view, gioView.postFrameCallback)
}
w.draw(env, false)
}
//export Java_org_gioui_GioView_onBack
@@ -882,6 +879,9 @@ func (w *window) draw(env *C.JNIEnv, sync bool) {
},
Sync: sync,
})
if w.animating {
callVoidMethod(env, w.view, gioView.postFrameCallback)
}
a11yActive, err := callBooleanMethod(env, w.view, gioView.isA11yActive)
if err != nil {
panic(err)
@@ -1495,3 +1495,6 @@ func Java_org_gioui_Gio_scheduleMainFuncs(env *C.JNIEnv, cls C.jclass) {
func (AndroidViewEvent) implementsViewEvent() {}
func (AndroidViewEvent) ImplementsEvent() {}
func (a AndroidViewEvent) Valid() bool {
return a != (AndroidViewEvent{})
}
+5 -1
View File
@@ -75,9 +75,13 @@ var displayLinks sync.Map
var mainFuncs = make(chan func(), 1)
func isMainThread() bool {
return bool(C.isMainThread())
}
// runOnMain runs the function on the main thread.
func runOnMain(f func()) {
if C.isMainThread() {
if isMainThread() {
f()
return
}
+41 -1
View File
@@ -12,6 +12,7 @@ package app
#include <UIKit/UIKit.h>
#include <stdint.h>
__attribute__ ((visibility ("hidden"))) int gio_applicationMain(int argc, char *argv[]);
__attribute__ ((visibility ("hidden"))) void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle);
struct drawParams {
@@ -81,6 +82,7 @@ import "C"
import (
"image"
"io"
"os"
"runtime"
"runtime/cgo"
"runtime/debug"
@@ -394,13 +396,51 @@ func newWindow(win *callbacks, options []Option) {
<-mainWindow.windows
}
var mainMode = mainModeUndefined
const (
mainModeUndefined = iota
mainModeExe
mainModeLibrary
)
func osMain() {
if !isMainThread() {
panic("app.Main must be run on the main goroutine")
}
switch mainMode {
case mainModeUndefined:
mainMode = mainModeExe
var argv []*C.char
for _, arg := range os.Args {
a := C.CString(arg)
defer C.free(unsafe.Pointer(a))
argv = append(argv, a)
}
C.gio_applicationMain(C.int(len(argv)), unsafe.SliceData(argv))
case mainModeExe:
panic("app.Main may be called only once")
case mainModeLibrary:
// Do nothing, we're embedded as a library.
}
}
//export gio_runMain
func gio_runMain() {
runMain()
if !isMainThread() {
panic("app.Main must be run on the main goroutine")
}
switch mainMode {
case mainModeUndefined:
mainMode = mainModeLibrary
runMain()
case mainModeExe:
// Do nothing, main has already been called.
}
}
func (UIKitViewEvent) implementsViewEvent() {}
func (UIKitViewEvent) ImplementsEvent() {}
func (u UIKitViewEvent) Valid() bool {
return u != (UIKitViewEvent{})
}
+20
View File
@@ -280,3 +280,23 @@ void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle) {
GioView *v = (__bridge GioView *)viewRef;
v.handle = handle;
}
@interface _gioAppDelegate : UIResponder <UIApplicationDelegate>
@property (strong, nonatomic) UIWindow *window;
@end
@implementation _gioAppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
GioViewController *controller = [[GioViewController alloc] initWithNibName:nil bundle:nil];
self.window.rootViewController = controller;
[self.window makeKeyAndVisible];
return YES;
}
@end
int gio_applicationMain(int argc, char *argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([_gioAppDelegate class]));
}
}
+3
View File
@@ -822,3 +822,6 @@ func translateKey(k string) (key.Name, bool) {
func (JSViewEvent) implementsViewEvent() {}
func (JSViewEvent) ImplementsEvent() {}
func (j JSViewEvent) Valid() bool {
return !(j.Element.IsNull() || j.Element.IsUndefined())
}
+243 -142
View File
@@ -40,7 +40,7 @@ import (
#define MOUSE_SCROLL 4
__attribute__ ((visibility ("hidden"))) void gio_main(void);
__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createView(void);
__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createView(int presentWithTrans);
__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createWindow(CFTypeRef viewRef, CGFloat width, CGFloat height, CGFloat minWidth, CGFloat minHeight, CGFloat maxWidth, CGFloat maxHeight);
__attribute__ ((visibility ("hidden"))) void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle);
@@ -109,6 +109,14 @@ static void makeKeyAndOrderFront(CFTypeRef windowRef) {
}
}
static void makeFirstResponder(CFTypeRef windowRef, CFTypeRef viewRef) {
@autoreleasepool {
NSWindow *window = (__bridge NSWindow *)windowRef;
NSView *view = (__bridge NSView *)viewRef;
[window makeFirstResponder:view];
}
}
static void toggleFullScreen(CFTypeRef windowRef) {
@autoreleasepool {
NSWindow *window = (__bridge NSWindow *)windowRef;
@@ -238,6 +246,13 @@ static int isWindowZoomed(CFTypeRef windowRef) {
}
}
static int isWindowMiniaturized(CFTypeRef windowRef) {
@autoreleasepool {
NSWindow *window = (__bridge NSWindow *)windowRef;
return window.miniaturized ? 1 : 0;
}
}
static void zoomWindow(CFTypeRef windowRef) {
@autoreleasepool {
NSWindow *window = (__bridge NSWindow *)windowRef;
@@ -297,6 +312,21 @@ static void invalidateCharacterCoordinates(CFTypeRef viewRef) {
}
}
}
static void interpretKeyEvents(CFTypeRef viewRef, CFTypeRef eventRef) {
@autoreleasepool {
NSView *view = (__bridge NSView *)viewRef;
NSEvent *event = (__bridge NSEvent *)eventRef;
[view interpretKeyEvents:[NSArray arrayWithObject:event]];
}
}
static int isMiniaturized(CFTypeRef windowRef) {
@autoreleasepool {
NSWindow *window = (__bridge NSWindow *)windowRef;
return window.miniaturized ? 1 : 0;
}
}
*/
import "C"
@@ -318,7 +348,6 @@ type window struct {
view C.CFTypeRef
w *callbacks
anim bool
visible bool
displayLink *displayLink
// redraw is a single entry channel for making sure only one
// display link redraw request is in flight.
@@ -326,9 +355,20 @@ type window struct {
cursor pointer.Cursor
pointerBtns pointer.Buttons
loop *eventLoop
lastMods C.NSUInteger
scale float32
config Config
keysDown map[key.Name]struct{}
// cmdKeys is for storing the current key event while
// waiting for a doCommandBySelector.
cmdKeys cmdKeys
}
type cmdKeys struct {
eventStr string
eventMods key.Modifiers
}
// launched is closed when applicationDidFinishLaunching is called.
@@ -367,11 +407,23 @@ func (w *window) WriteClipboard(mime string, s []byte) {
}
func (w *window) updateWindowMode() {
w.scale = float32(C.getViewBackingScale(w.view))
wf, hf := float32(C.viewWidth(w.view)), float32(C.viewHeight(w.view))
w.config.Size = image.Point{
X: int(wf*w.scale + .5),
Y: int(hf*w.scale + .5),
}
w.config.Mode = Windowed
window := C.windowForView(w.view)
if window == 0 {
return
}
style := int(C.getWindowStyleMask(C.windowForView(w.view)))
if style&C.NSWindowStyleMaskFullScreen != 0 {
switch {
case style&C.NSWindowStyleMaskFullScreen != 0:
w.config.Mode = Fullscreen
} else {
w.config.Mode = Windowed
case C.isWindowZoomed(window) != 0:
w.config.Mode = Maximized
}
w.config.Decorated = style&C.NSWindowStyleMaskFullSizeContentView == 0
}
@@ -379,105 +431,82 @@ func (w *window) updateWindowMode() {
func (w *window) Configure(options []Option) {
screenScale := float32(C.getScreenBackingScale())
cfg := configFor(screenScale)
prev := w.config
w.updateWindowMode()
cnf := w.config
cnf.apply(cfg, options)
window := C.windowForView(w.view)
mask := C.getWindowStyleMask(window)
fullscreen := mask&C.NSWindowStyleMaskFullScreen != 0
switch cnf.Mode {
case Fullscreen:
switch prev.Mode {
case Fullscreen:
case Minimized:
if C.isWindowMiniaturized(window) != 0 {
C.unhideWindow(window)
fallthrough
default:
w.config.Mode = Fullscreen
}
if !fullscreen {
C.toggleFullScreen(window)
}
case Minimized:
switch prev.Mode {
case Minimized, Fullscreen:
default:
w.config.Mode = Minimized
C.hideWindow(window)
}
C.hideWindow(window)
case Maximized:
switch prev.Mode {
case Fullscreen:
case Minimized:
if C.isWindowMiniaturized(window) != 0 {
C.unhideWindow(window)
fallthrough
default:
w.config.Mode = Maximized
w.setTitle(prev, cnf)
if C.isWindowZoomed(window) == 0 {
C.zoomWindow(window)
}
}
if fullscreen {
C.toggleFullScreen(window)
}
w.setTitle(cnf.Title)
if C.isWindowZoomed(window) == 0 {
C.zoomWindow(window)
}
case Windowed:
switch prev.Mode {
case Fullscreen:
C.toggleFullScreen(window)
case Minimized:
if C.isWindowMiniaturized(window) != 0 {
C.unhideWindow(window)
case Maximized:
if C.isWindowZoomed(window) != 0 {
C.zoomWindow(window)
}
}
w.config.Mode = Windowed
w.setTitle(prev, cnf)
if prev.Size != cnf.Size {
w.config.Size = cnf.Size
cnf.Size = cnf.Size.Div(int(screenScale))
C.setSize(window, C.CGFloat(cnf.Size.X), C.CGFloat(cnf.Size.Y))
if fullscreen {
C.toggleFullScreen(window)
}
if prev.MinSize != cnf.MinSize {
w.config.MinSize = cnf.MinSize
cnf.MinSize = cnf.MinSize.Div(int(screenScale))
C.setMinSize(window, C.CGFloat(cnf.MinSize.X), C.CGFloat(cnf.MinSize.Y))
}
if prev.MaxSize != cnf.MaxSize {
w.config.MaxSize = cnf.MaxSize
cnf.MaxSize = cnf.MaxSize.Div(int(screenScale))
w.setTitle(cnf.Title)
w.config.Size = cnf.Size
cnf.Size = cnf.Size.Div(int(screenScale))
C.setSize(window, C.CGFloat(cnf.Size.X), C.CGFloat(cnf.Size.Y))
w.config.MinSize = cnf.MinSize
cnf.MinSize = cnf.MinSize.Div(int(screenScale))
C.setMinSize(window, C.CGFloat(cnf.MinSize.X), C.CGFloat(cnf.MinSize.Y))
w.config.MaxSize = cnf.MaxSize
cnf.MaxSize = cnf.MaxSize.Div(int(screenScale))
if cnf.MaxSize != (image.Point{}) {
C.setMaxSize(window, C.CGFloat(cnf.MaxSize.X), C.CGFloat(cnf.MaxSize.Y))
}
}
if cnf.Decorated != prev.Decorated {
w.config.Decorated = cnf.Decorated
mask := C.getWindowStyleMask(window)
style := C.NSWindowStyleMask(C.NSWindowStyleMaskTitled | C.NSWindowStyleMaskResizable | C.NSWindowStyleMaskMiniaturizable | C.NSWindowStyleMaskClosable)
style = C.NSWindowStyleMaskFullSizeContentView
mask &^= style
barTrans := C.int(C.NO)
titleVis := C.NSWindowTitleVisibility(C.NSWindowTitleVisible)
if !cnf.Decorated {
mask |= style
barTrans = C.YES
titleVis = C.NSWindowTitleHidden
if C.isWindowZoomed(window) != 0 {
C.zoomWindow(window)
}
C.setWindowTitlebarAppearsTransparent(window, barTrans)
C.setWindowTitleVisibility(window, titleVis)
C.setWindowStyleMask(window, mask)
C.setWindowStandardButtonHidden(window, C.NSWindowCloseButton, barTrans)
C.setWindowStandardButtonHidden(window, C.NSWindowMiniaturizeButton, barTrans)
C.setWindowStandardButtonHidden(window, C.NSWindowZoomButton, barTrans)
// When toggling the titlebar, the layer doesn't update its frame
// until the next resize. Force it.
C.resetLayerFrame(w.view)
}
w.ProcessEvent(ConfigEvent{Config: w.config})
style := C.NSWindowStyleMask(C.NSWindowStyleMaskTitled | C.NSWindowStyleMaskResizable | C.NSWindowStyleMaskMiniaturizable | C.NSWindowStyleMaskClosable)
style = C.NSWindowStyleMaskFullSizeContentView
mask &^= style
barTrans := C.int(C.NO)
titleVis := C.NSWindowTitleVisibility(C.NSWindowTitleVisible)
if !cnf.Decorated {
mask |= style
barTrans = C.YES
titleVis = C.NSWindowTitleHidden
}
C.setWindowTitlebarAppearsTransparent(window, barTrans)
C.setWindowTitleVisibility(window, titleVis)
C.setWindowStyleMask(window, mask)
C.setWindowStandardButtonHidden(window, C.NSWindowCloseButton, barTrans)
C.setWindowStandardButtonHidden(window, C.NSWindowMiniaturizeButton, barTrans)
C.setWindowStandardButtonHidden(window, C.NSWindowZoomButton, barTrans)
// When toggling the titlebar, the layer doesn't update its frame
// until the next resize. Force it.
C.resetLayerFrame(w.view)
}
func (w *window) setTitle(prev, cnf Config) {
if prev.Title != cnf.Title {
w.config.Title = cnf.Title
title := stringToNSString(cnf.Title)
defer C.CFRelease(title)
C.setTitle(C.windowForView(w.view), title)
}
func (w *window) setTitle(title string) {
w.config.Title = title
titleC := stringToNSString(title)
defer C.CFRelease(titleC)
C.setTitle(C.windowForView(w.view), titleC)
}
func (w *window) Perform(acts system.Action) {
@@ -520,7 +549,8 @@ func (w *window) SetInputHint(_ key.InputHint) {}
func (w *window) SetAnimating(anim bool) {
w.anim = anim
if w.anim && w.visible {
window := C.windowForView(w.view)
if w.anim && window != 0 && C.isMiniaturized(window) == 0 {
w.displayLink.Start()
} else {
w.displayLink.Stop()
@@ -538,23 +568,92 @@ func (w *window) runOnMain(f func()) {
}
//export gio_onKeys
func gio_onKeys(h C.uintptr_t, cstr C.CFTypeRef, ti C.double, mods C.NSUInteger, keyDown C.bool) {
func gio_onKeys(h C.uintptr_t, event C.CFTypeRef, cstr C.CFTypeRef, ti C.double, mods C.NSUInteger, keyDown C.bool) {
w := windowFor(h)
if w.keysDown == nil {
w.keysDown = make(map[key.Name]struct{})
}
str := nsstringToString(cstr)
kmods := convertMods(mods)
ks := key.Release
if keyDown {
ks = key.Press
w.cmdKeys.eventStr = str
w.cmdKeys.eventMods = kmods
C.interpretKeyEvents(w.view, event)
}
w := windowFor(h)
for _, k := range str {
if n, ok := convertKey(k); ok {
w.ProcessEvent(key.Event{
ke := key.Event{
Name: n,
Modifiers: kmods,
State: ks,
}
if keyDown {
w.keysDown[ke.Name] = struct{}{}
if _, isCmd := convertCommandKey(k); isCmd || kmods.Contain(key.ModCommand) {
// doCommandBySelector already processed the event.
return
}
} else {
if _, pressed := w.keysDown[n]; !pressed {
continue
}
delete(w.keysDown, n)
}
w.ProcessEvent(ke)
}
}
}
//export gio_onCommandBySelector
func gio_onCommandBySelector(h C.uintptr_t) C.bool {
w := windowFor(h)
ev := w.cmdKeys
w.cmdKeys = cmdKeys{}
handled := false
for _, k := range ev.eventStr {
n, ok := convertCommandKey(k)
if !ok && ev.eventMods.Contain(key.ModCommand) {
n, ok = convertKey(k)
}
if !ok {
continue
}
ke := key.Event{
Name: n,
Modifiers: ev.eventMods,
State: key.Press,
}
handled = w.processEvent(ke) || handled
}
return C.bool(handled)
}
//export gio_onFlagsChanged
func gio_onFlagsChanged(h C.uintptr_t, curMods C.NSUInteger) {
w := windowFor(h)
mods := []C.NSUInteger{C.NSControlKeyMask, C.NSAlternateKeyMask, C.NSShiftKeyMask, C.NSCommandKeyMask}
keys := []key.Name{key.NameCtrl, key.NameAlt, key.NameShift, key.NameCommand}
for i, mod := range mods {
wasPressed := w.lastMods&mod != 0
isPressed := curMods&mod != 0
if wasPressed != isPressed {
st := key.Release
if isPressed {
st = key.Press
}
w.ProcessEvent(key.Event{
Name: keys[i],
State: st,
})
}
}
w.lastMods = curMods
}
//export gio_onText
@@ -747,6 +846,15 @@ func gio_substringForProposedRange(h C.uintptr_t, crng C.NSRange, actual C.NSRan
//export gio_insertText
func gio_insertText(h C.uintptr_t, cstr C.CFTypeRef, crng C.NSRange) {
w := windowFor(h)
str := nsstringToString(cstr)
// macOS IME in some cases calls insertText for command keys such as backspace
// instead of doCommandBySelector.
for _, r := range str {
if _, ok := convertCommandKey(r); ok {
w.w.SetComposingRegion(key.Range{Start: -1, End: -1})
return
}
}
state := w.w.EditorState()
rng := state.compose
if rng.Start == -1 {
@@ -758,7 +866,6 @@ func gio_insertText(h C.uintptr_t, cstr C.CFTypeRef, crng C.NSRange) {
End: state.RunesIndex(int(crng.location + crng.length)),
}
}
str := nsstringToString(cstr)
w.w.EditorReplace(rng, str)
w.w.SetComposingRegion(key.Range{Start: -1, End: -1})
start := rng.Start
@@ -799,24 +906,19 @@ func gio_firstRectForCharacterRange(h C.uintptr_t, crng C.NSRange, actual C.NSRa
}
func (w *window) draw() {
cnf := w.config
w.updateWindowMode()
if w.config != cnf {
w.ProcessEvent(ConfigEvent{Config: w.config})
}
select {
case <-w.redraw:
default:
}
w.visible = true
if w.anim {
w.SetAnimating(w.anim)
}
w.scale = float32(C.getViewBackingScale(w.view))
wf, hf := float32(C.viewWidth(w.view)), float32(C.viewHeight(w.view))
sz := image.Point{
X: int(wf*w.scale + .5),
Y: int(hf*w.scale + .5),
}
if sz != w.config.Size {
w.config.Size = sz
w.ProcessEvent(ConfigEvent{Config: w.config})
}
sz := w.config.Size
if sz.X == 0 || sz.Y == 0 {
return
}
@@ -824,7 +926,7 @@ func (w *window) draw() {
w.ProcessEvent(frameEvent{
FrameEvent: FrameEvent{
Now: time.Now(),
Size: w.config.Size,
Size: sz,
Metric: cfg,
},
Sync: true,
@@ -832,8 +934,13 @@ func (w *window) draw() {
}
func (w *window) ProcessEvent(e event.Event) {
w.w.ProcessEvent(e)
w.processEvent(e)
}
func (w *window) processEvent(e event.Event) bool {
handled := w.w.ProcessEvent(e)
w.loop.FlushEvents()
return handled
}
func (w *window) Event() event.Event {
@@ -867,7 +974,6 @@ func gio_onAttached(h C.uintptr_t, attached C.int) {
w.ProcessEvent(AppKitViewEvent{View: uintptr(w.view), Layer: uintptr(layer)})
} else {
w.ProcessEvent(AppKitViewEvent{})
w.visible = false
w.SetAnimating(w.anim)
}
}
@@ -882,33 +988,6 @@ func gio_onDestroy(h C.uintptr_t) {
w.view = 0
}
//export gio_onHide
func gio_onHide(h C.uintptr_t) {
w := windowFor(h)
w.visible = false
w.SetAnimating(w.anim)
}
//export gio_onShow
func gio_onShow(h C.uintptr_t) {
w := windowFor(h)
w.draw()
}
//export gio_onFullscreen
func gio_onFullscreen(h C.uintptr_t) {
w := windowFor(h)
w.config.Mode = Fullscreen
w.ProcessEvent(ConfigEvent{Config: w.config})
}
//export gio_onWindowed
func gio_onWindowed(h C.uintptr_t) {
w := windowFor(h)
w.config.Mode = Windowed
w.ProcessEvent(ConfigEvent{Config: w.config})
}
//export gio_onFinishLaunching
func gio_onFinishLaunching() {
close(launched)
@@ -925,14 +1004,15 @@ func newWindow(win *callbacks, options []Option) {
w.loop = newEventLoop(w.w, w.wakeup)
win.SetDriver(w)
res <- struct{}{}
if err := w.init(); err != nil {
var cnf Config
cnf.apply(unit.Metric{}, options)
if err := w.init(cnf.CustomRenderer); err != nil {
w.ProcessEvent(DestroyEvent{Err: err})
return
}
window := C.gio_createWindow(w.view, 0, 0, 0, 0, 0, 0)
window := C.gio_createWindow(w.view, C.CGFloat(cnf.Size.X), C.CGFloat(cnf.Size.Y), 0, 0, 0, 0)
// Release our reference now that the NSWindow has it.
C.CFRelease(w.view)
w.updateWindowMode()
w.Configure(options)
if nextTopLeft.x == 0 && nextTopLeft.y == 0 {
// cascadeTopLeftFromPoint treats (0, 0) as a no-op,
@@ -940,14 +1020,19 @@ func newWindow(win *callbacks, options []Option) {
nextTopLeft = C.cascadeTopLeftFromPoint(window, nextTopLeft)
}
nextTopLeft = C.cascadeTopLeftFromPoint(window, nextTopLeft)
C.makeFirstResponder(window, w.view)
// makeKeyAndOrderFront assumes ownership of our window reference.
C.makeKeyAndOrderFront(window)
})
<-res
}
func (w *window) init() error {
view := C.gio_createView()
func (w *window) init(customRenderer bool) error {
presentWithTrans := 1
if customRenderer {
presentWithTrans = 0
}
view := C.gio_createView(C.int(presentWithTrans))
if view == 0 {
return errors.New("newOSWindow: failed to create view")
}
@@ -960,9 +1045,7 @@ func (w *window) init() error {
return
}
w.runOnMain(func() {
if w.visible {
C.setNeedsDisplay(w.view)
}
C.setNeedsDisplay(w.view)
})
})
w.displayLink = dl
@@ -976,13 +1059,16 @@ func (w *window) init() error {
}
func osMain() {
if !isMainThread() {
panic("app.Main must run on the main goroutine")
}
C.gio_main()
}
func convertKey(k rune) (key.Name, bool) {
func convertCommandKey(k rune) (key.Name, bool) {
var n key.Name
switch k {
case 0x1b:
case '\x1b': // ASCII escape.
n = key.NameEscape
case C.NSLeftArrowFunctionKey:
n = key.NameLeftArrow
@@ -992,22 +1078,36 @@ func convertKey(k rune) (key.Name, bool) {
n = key.NameUpArrow
case C.NSDownArrowFunctionKey:
n = key.NameDownArrow
case 0xd:
case '\r':
n = key.NameReturn
case 0x3:
case '\x03':
n = key.NameEnter
case C.NSHomeFunctionKey:
n = key.NameHome
case C.NSEndFunctionKey:
n = key.NameEnd
case 0x7f:
case '\x7f', '\b':
n = key.NameDeleteBackward
case C.NSDeleteFunctionKey:
n = key.NameDeleteForward
case '\t', 0x19:
n = key.NameTab
case C.NSPageUpFunctionKey:
n = key.NamePageUp
case C.NSPageDownFunctionKey:
n = key.NamePageDown
default:
return "", false
}
return n, true
}
func convertKey(k rune) (key.Name, bool) {
if n, ok := convertCommandKey(k); ok {
return n, true
}
var n key.Name
switch k {
case C.NSF1FunctionKey:
n = key.NameF1
case C.NSF2FunctionKey:
@@ -1032,8 +1132,6 @@ func convertKey(k rune) (key.Name, bool) {
n = key.NameF11
case C.NSF12FunctionKey:
n = key.NameF12
case 0x09, 0x19:
n = key.NameTab
case 0x20:
n = key.NameSpace
default:
@@ -1065,3 +1163,6 @@ func convertMods(mods C.NSUInteger) key.Modifiers {
func (AppKitViewEvent) implementsViewEvent() {}
func (AppKitViewEvent) ImplementsEvent() {}
func (a AppKitViewEvent) Valid() bool {
return a != (AppKitViewEvent{})
}
+37 -21
View File
@@ -6,7 +6,7 @@
#include "_cgo_export.h"
__attribute__ ((visibility ("hidden"))) CALayer *gio_layerFactory(void);
__attribute__ ((visibility ("hidden"))) CALayer *gio_layerFactory(BOOL presentWithTrans);
@interface GioAppDelegate : NSObject<NSApplicationDelegate>
@end
@@ -16,28 +16,29 @@ __attribute__ ((visibility ("hidden"))) CALayer *gio_layerFactory(void);
@interface GioView : NSView <CALayerDelegate,NSTextInputClient>
@property uintptr_t handle;
@property BOOL presentWithTrans;
@end
@implementation GioWindowDelegate
- (void)windowWillMiniaturize:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object];
GioView *view = (GioView *)window.contentView;
gio_onHide(view.handle);
gio_onDraw(view.handle);
}
- (void)windowDidDeminiaturize:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object];
GioView *view = (GioView *)window.contentView;
gio_onShow(view.handle);
gio_onDraw(view.handle);
}
- (void)windowWillEnterFullScreen:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object];
GioView *view = (GioView *)window.contentView;
gio_onFullscreen(view.handle);
gio_onDraw(view.handle);
}
- (void)windowWillExitFullScreen:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object];
GioView *view = (GioView *)window.contentView;
gio_onWindowed(view.handle);
gio_onDraw(view.handle);
}
- (void)windowDidChangeScreen:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object];
@@ -47,13 +48,17 @@ __attribute__ ((visibility ("hidden"))) CALayer *gio_layerFactory(void);
}
- (void)windowDidBecomeKey:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object];
GioView *view = (GioView *)window.contentView;
gio_onFocus(view.handle, 1);
GioView *view = (GioView *)window.contentView;
if ([window firstResponder] == view) {
gio_onFocus(view.handle, 1);
}
}
- (void)windowDidResignKey:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object];
GioView *view = (GioView *)window.contentView;
gio_onFocus(view.handle, 0);
GioView *view = (GioView *)window.contentView;
if ([window firstResponder] == view) {
gio_onFocus(view.handle, 0);
}
}
@end
@@ -84,7 +89,7 @@ static void handleMouse(GioView *view, NSEvent *event, int typ, CGFloat dx, CGFl
gio_onDraw(self.handle);
}
- (CALayer *)makeBackingLayer {
CALayer *layer = gio_layerFactory();
CALayer *layer = gio_layerFactory(self.presentWithTrans);
layer.delegate = self;
return layer;
}
@@ -127,22 +132,25 @@ static void handleMouse(GioView *view, NSEvent *event, int typ, CGFloat dx, CGFl
handleMouse(self, event, MOUSE_SCROLL, dx, dy);
}
- (void)keyDown:(NSEvent *)event {
[self interpretKeyEvents:[NSArray arrayWithObject:event]];
NSString *keys = [event charactersIgnoringModifiers];
gio_onKeys(self.handle, (__bridge CFTypeRef)keys, [event timestamp], [event modifierFlags], true);
gio_onKeys(self.handle, (__bridge CFTypeRef)event, (__bridge CFTypeRef)keys, [event timestamp], [event modifierFlags], true);
}
- (void)flagsChanged:(NSEvent *)event {
[self interpretKeyEvents:[NSArray arrayWithObject:event]];
gio_onFlagsChanged(self.handle, [event modifierFlags]);
}
- (void)keyUp:(NSEvent *)event {
NSString *keys = [event charactersIgnoringModifiers];
gio_onKeys(self.handle, (__bridge CFTypeRef)keys, [event timestamp], [event modifierFlags], false);
gio_onKeys(self.handle, (__bridge CFTypeRef)event, (__bridge CFTypeRef)keys, [event timestamp], [event modifierFlags], false);
}
- (void)insertText:(id)string {
gio_onText(self.handle, (__bridge CFTypeRef)string);
}
- (void)doCommandBySelector:(SEL)sel {
// Don't pass commands up the responder chain.
// They will end up in a beep.
- (void)doCommandBySelector:(SEL)action {
if (!gio_onCommandBySelector(self.handle)) {
[super doCommandBySelector:action];
}
}
- (BOOL)hasMarkedText {
int res = gio_hasMarkedText(self.handle);
return res ? YES : NO;
@@ -197,14 +205,22 @@ static void handleMouse(GioView *view, NSEvent *event, int typ, CGFloat dx, CGFl
return [[self window] convertRectToScreen:r];
}
- (void)applicationWillUnhide:(NSNotification *)notification {
gio_onShow(self.handle);
gio_onDraw(self.handle);
}
- (void)applicationDidHide:(NSNotification *)notification {
gio_onHide(self.handle);
gio_onDraw(self.handle);
}
- (void)dealloc {
gio_onDestroy(self.handle);
}
- (BOOL) becomeFirstResponder {
gio_onFocus(self.handle, 1);
return [super becomeFirstResponder];
}
- (BOOL) resignFirstResponder {
gio_onFocus(self.handle, 0);
return [super resignFirstResponder];
}
@end
// Delegates are weakly referenced from their peers. Nothing
@@ -374,16 +390,16 @@ CFTypeRef gio_createWindow(CFTypeRef viewRef, CGFloat width, CGFloat height, CGF
[window setAcceptsMouseMovedEvents:YES];
NSView *view = (__bridge NSView *)viewRef;
[window setContentView:view];
[window makeFirstResponder:view];
window.delegate = globalWindowDel;
return (__bridge_retained CFTypeRef)window;
}
}
CFTypeRef gio_createView(void) {
CFTypeRef gio_createView(int presentWithTrans) {
@autoreleasepool {
NSRect frame = NSMakeRect(0, 0, 0, 0);
GioView* view = [[GioView alloc] initWithFrame:frame];
view.presentWithTrans = presentWithTrans ? YES : NO;
view.wantsLayer = YES;
view.layerContentsRedrawPolicy = NSViewLayerContentsRedrawDuringViewResize;
+6 -24
View File
@@ -9,7 +9,6 @@ import (
"errors"
"unsafe"
"gioui.org/io/event"
"gioui.org/io/pointer"
)
@@ -22,6 +21,9 @@ type X11ViewEvent struct {
func (X11ViewEvent) implementsViewEvent() {}
func (X11ViewEvent) ImplementsEvent() {}
func (x X11ViewEvent) Valid() bool {
return x != (X11ViewEvent{})
}
type WaylandViewEvent struct {
// Display is the *wl_display returned by wl_display_connect.
@@ -32,6 +34,9 @@ type WaylandViewEvent struct {
func (WaylandViewEvent) implementsViewEvent() {}
func (WaylandViewEvent) ImplementsEvent() {}
func (w WaylandViewEvent) Valid() bool {
return w != (WaylandViewEvent{})
}
func osMain() {
select {}
@@ -57,35 +62,12 @@ func newWindow(window *callbacks, options []Option) {
errFirst = err
}
}
window.SetDriver(&dummyDriver{
win: window,
wakeups: make(chan event.Event, 1),
})
if errFirst == nil {
errFirst = errors.New("app: no window driver available")
}
window.ProcessEvent(DestroyEvent{Err: errFirst})
}
type dummyDriver struct {
win *callbacks
wakeups chan event.Event
}
func (d *dummyDriver) Event() event.Event {
if e, ok := d.win.nextEvent(); ok {
return e
}
return <-d.wakeups
}
func (d *dummyDriver) Invalidate() {
select {
case d.wakeups <- wakeupEvent{}:
default:
}
}
// xCursor contains mapping from pointer.Cursor to XCursor.
var xCursor = [...]string{
pointer.CursorDefault: "left_ptr",
+20 -24
View File
@@ -216,9 +216,7 @@ type window struct {
wakeups chan struct{}
// invMu avoids the race between the destruction of disp and
// Invalidate waking it up.
invMu sync.Mutex
closing bool
}
type poller struct {
@@ -556,7 +554,7 @@ func gio_onXdgSurfaceConfigure(data unsafe.Pointer, wmSurf *C.struct_xdg_surface
//export gio_onToplevelClose
func gio_onToplevelClose(data unsafe.Pointer, topLvl *C.struct_xdg_toplevel) {
w := callbackLoad(data).(*window)
w.close(nil)
w.closing = true
}
//export gio_onToplevelConfigure
@@ -1139,7 +1137,7 @@ func (w *window) Perform(actions system.Action) {
walkActions(actions, func(action system.Action) {
switch action {
case system.ActionClose:
w.close(nil)
w.closing = true
}
})
}
@@ -1366,6 +1364,9 @@ func gio_onFrameDone(data unsafe.Pointer, callback *C.struct_wl_callback, t C.ui
func (w *window) close(err error) {
w.ProcessEvent(WaylandViewEvent{})
w.ProcessEvent(DestroyEvent{Err: err})
w.destroy()
w.disp.destroy()
w.disp = nil
}
func (w *window) dispatch() {
@@ -1374,7 +1375,7 @@ func (w *window) dispatch() {
w.w.Invalidate()
return
}
if err := w.disp.dispatch(); err != nil {
if err := w.disp.dispatch(); err != nil || w.closing {
w.close(err)
return
}
@@ -1399,13 +1400,6 @@ func (w *window) Event() event.Event {
w.dispatch()
continue
}
if _, destroy := evt.(DestroyEvent); destroy {
w.destroy()
w.invMu.Lock()
w.disp.destroy()
w.disp = nil
w.invMu.Unlock()
}
return evt
}
}
@@ -1416,11 +1410,7 @@ func (w *window) Invalidate() {
default:
return
}
w.invMu.Lock()
defer w.invMu.Unlock()
if w.disp != nil {
w.disp.wakeup()
}
w.disp.wakeup()
}
func (w *window) Run(f func()) {
@@ -1515,6 +1505,10 @@ func (d *wlDisplay) wakeup() {
}
func (w *window) destroy() {
if w.lastFrameCallback != nil {
C.wl_callback_destroy(w.lastFrameCallback)
w.lastFrameCallback = nil
}
if w.cursor.surf != nil {
C.wl_surface_destroy(w.cursor.surf)
}
@@ -1639,6 +1633,14 @@ func (w *window) flushScroll() {
if total == (f32.Point{}) {
return
}
if w.scroll.steps == (image.Point{}) {
w.fling.xExtrapolation.SampleDelta(w.scroll.time, -w.scroll.dist.X)
w.fling.yExtrapolation.SampleDelta(w.scroll.time, -w.scroll.dist.Y)
}
// Zero scroll distance prior to calling ProcessEvent, otherwise we may recursively
// re-process the scroll distance.
w.scroll.dist = f32.Point{}
w.scroll.steps = image.Point{}
w.ProcessEvent(pointer.Event{
Kind: pointer.Scroll,
Source: pointer.Mouse,
@@ -1648,12 +1650,6 @@ func (w *window) flushScroll() {
Time: w.scroll.time,
Modifiers: w.disp.xkb.Modifiers(),
})
if w.scroll.steps == (image.Point{}) {
w.fling.xExtrapolation.SampleDelta(w.scroll.time, -w.scroll.dist.X)
w.fling.yExtrapolation.SampleDelta(w.scroll.time, -w.scroll.dist.Y)
}
w.scroll.dist = f32.Point{}
w.scroll.steps = image.Point{}
}
func (w *window) onPointerMotion(x, y C.wl_fixed_t, t C.uint32_t) {
+59 -59
View File
@@ -46,17 +46,13 @@ type window struct {
cursorIn bool
cursor syscall.Handle
// placement saves the previous window position when in full screen mode.
placement *windows.WindowPlacement
animating bool
borderSize image.Point
config Config
loop *eventLoop
// invMu avoids the race between destroying the window and Invalidate.
invMu sync.Mutex
// frameDims stores the last seen window frame width and height.
frameDims image.Point
loop *eventLoop
}
const _WM_WAKEUP = windows.WM_USER + iota
@@ -111,8 +107,8 @@ func newWindow(win *callbacks, options []Option) {
}
winMap.Store(w.hwnd, w)
defer winMap.Delete(w.hwnd)
w.ProcessEvent(Win32ViewEvent{HWND: uintptr(w.hwnd)})
w.Configure(options)
w.ProcessEvent(Win32ViewEvent{HWND: uintptr(w.hwnd)})
windows.SetForegroundWindow(w.hwnd)
windows.SetFocus(w.hwnd)
// Since the window class for the cursor is null,
@@ -188,21 +184,39 @@ func (w *window) init() error {
return nil
}
// update() handles changes done by the user, and updates the configuration.
// update handles changes done by the user, and updates the configuration.
// It reads the window style and size/position and updates w.config.
// If anything has changed it emits a ConfigEvent to notify the application.
func (w *window) update() {
cr := windows.GetClientRect(w.hwnd)
w.config.Size = image.Point{
X: int(cr.Right - cr.Left),
Y: int(cr.Bottom - cr.Top),
p := windows.GetWindowPlacement(w.hwnd)
if !p.IsMinimized() {
r := windows.GetWindowRect(w.hwnd)
cr := windows.GetClientRect(w.hwnd)
w.config.Size = image.Point{
X: int(cr.Right - cr.Left),
Y: int(cr.Bottom - cr.Top),
}
w.frameDims = image.Point{
X: int(r.Right - r.Left),
Y: int(r.Bottom - r.Top),
}.Sub(w.config.Size)
}
w.borderSize = image.Pt(
windows.GetSystemMetrics(windows.SM_CXSIZEFRAME),
windows.GetSystemMetrics(windows.SM_CYSIZEFRAME),
)
style := windows.GetWindowLong(w.hwnd, windows.GWL_STYLE)
switch {
case p.IsMaximized() && style&windows.WS_OVERLAPPEDWINDOW != 0:
w.config.Mode = Maximized
case p.IsMaximized():
w.config.Mode = Fullscreen
default:
w.config.Mode = Windowed
}
w.ProcessEvent(ConfigEvent{Config: w.config})
w.draw(true)
}
func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr {
@@ -300,15 +314,15 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
case windows.WM_DESTROY:
w.ProcessEvent(Win32ViewEvent{})
w.ProcessEvent(DestroyEvent{})
w.w = nil
if w.hdc != 0 {
windows.ReleaseDC(w.hdc)
w.hdc = 0
}
w.invMu.Lock()
// The system destroys the HWND for us.
w.hwnd = 0
w.invMu.Unlock()
windows.PostQuitMessage(0)
return 0
case windows.WM_NCCALCSIZE:
if w.config.Decorated {
// Let Windows handle decorations.
@@ -333,37 +347,31 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
return 0
case windows.WM_PAINT:
w.draw(true)
case windows.WM_STYLECHANGED:
w.update()
case windows.WM_WINDOWPOSCHANGED:
w.update()
case windows.WM_SIZE:
w.update()
switch wParam {
case windows.SIZE_MINIMIZED:
w.config.Mode = Minimized
case windows.SIZE_MAXIMIZED:
w.config.Mode = Maximized
case windows.SIZE_RESTORED:
if w.config.Mode != Fullscreen {
w.config.Mode = Windowed
}
}
case windows.WM_GETMINMAXINFO:
mm := (*windows.MinMaxInfo)(unsafe.Pointer(lParam))
var bw, bh int32
var frameDims image.Point
if w.config.Decorated {
r := windows.GetWindowRect(w.hwnd)
cr := windows.GetClientRect(w.hwnd)
bw = r.Right - r.Left - (cr.Right - cr.Left)
bh = r.Bottom - r.Top - (cr.Bottom - cr.Top)
frameDims = w.frameDims
}
if p := w.config.MinSize; p.X > 0 || p.Y > 0 {
p = p.Add(frameDims)
mm.PtMinTrackSize = windows.Point{
X: int32(p.X) + bw,
Y: int32(p.Y) + bh,
X: int32(p.X),
Y: int32(p.Y),
}
}
if p := w.config.MaxSize; p.X > 0 || p.Y > 0 {
p = p.Add(frameDims)
mm.PtMaxTrackSize = windows.Point{
X: int32(p.X) + bw,
Y: int32(p.Y) + bh,
X: int32(p.X),
Y: int32(p.Y),
}
}
return 0
@@ -455,9 +463,6 @@ func getModifiers() key.Modifiers {
// hitTest returns the non-client area hit by the point, needed to
// process WM_NCHITTEST.
func (w *window) hitTest(x, y int) uintptr {
if w.config.Mode == Fullscreen {
return windows.HTCLIENT
}
if w.config.Mode != Windowed {
// Only windowed mode should allow resizing.
return windows.HTCLIENT
@@ -567,7 +572,8 @@ func (w *window) runLoop() {
loop:
for {
anim := w.animating
if anim && !windows.PeekMessage(msg, 0, 0, 0, windows.PM_NOREMOVE) {
p := windows.GetWindowPlacement(w.hwnd)
if anim && !p.IsMinimized() && !windows.PeekMessage(msg, 0, 0, 0, windows.PM_NOREMOVE) {
w.draw(false)
continue
}
@@ -620,13 +626,6 @@ func (w *window) Frame(frame *op.Ops) {
}
func (w *window) wakeup() {
w.invMu.Lock()
defer w.invMu.Unlock()
if w.hwnd == 0 {
w.loop.Wakeup()
w.loop.FlushEvents()
return
}
if err := windows.PostMessage(w.hwnd, _WM_WAKEUP, 0, 0); err != nil {
panic(err)
}
@@ -697,8 +696,13 @@ func (w *window) readClipboard() error {
func (w *window) Configure(options []Option) {
dpi := windows.GetSystemDPI()
metric := configForDPI(dpi)
w.config.apply(metric, options)
windows.SetWindowText(w.hwnd, w.config.Title)
cnf := w.config
cnf.apply(metric, options)
w.config.Title = cnf.Title
w.config.Decorated = cnf.Decorated
w.config.MinSize = cnf.MinSize
w.config.MaxSize = cnf.MaxSize
windows.SetWindowText(w.hwnd, cnf.Title)
style := windows.GetWindowLong(w.hwnd, windows.GWL_STYLE)
var showMode int32
@@ -706,7 +710,7 @@ func (w *window) Configure(options []Option) {
swpStyle := uintptr(windows.SWP_NOZORDER | windows.SWP_FRAMECHANGED)
winStyle := uintptr(windows.WS_OVERLAPPEDWINDOW)
style &^= winStyle
switch w.config.Mode {
switch cnf.Mode {
case Minimized:
style |= winStyle
swpStyle |= windows.SWP_NOMOVE | windows.SWP_NOSIZE
@@ -721,13 +725,13 @@ func (w *window) Configure(options []Option) {
style |= winStyle
showMode = windows.SW_SHOWNORMAL
// Get target for client area size.
width = int32(w.config.Size.X)
height = int32(w.config.Size.Y)
width = int32(cnf.Size.X)
height = int32(cnf.Size.Y)
// Get the current window size and position.
wr := windows.GetWindowRect(w.hwnd)
x = wr.Left
y = wr.Top
if w.config.Decorated {
if cnf.Decorated {
// Compute client size and position. Note that the client size is
// equal to the window size when we are in control of decorations.
r := windows.Rect{
@@ -737,25 +741,18 @@ func (w *window) Configure(options []Option) {
windows.AdjustWindowRectEx(&r, uint32(style), 0, dwExStyle)
width = r.Right - r.Left
height = r.Bottom - r.Top
}
if !w.config.Decorated {
} else {
// Enable drop shadows when we draw decorations.
windows.DwmExtendFrameIntoClientArea(w.hwnd, windows.Margins{-1, -1, -1, -1})
}
case Fullscreen:
swpStyle |= windows.SWP_NOMOVE | windows.SWP_NOSIZE
mi := windows.GetMonitorInfo(w.hwnd)
x, y = mi.Monitor.Left, mi.Monitor.Top
width = mi.Monitor.Right - mi.Monitor.Left
height = mi.Monitor.Bottom - mi.Monitor.Top
showMode = windows.SW_SHOWMAXIMIZED
}
windows.SetWindowLong(w.hwnd, windows.GWL_STYLE, style)
windows.SetWindowPos(w.hwnd, 0, x, y, width, height, swpStyle)
windows.ShowWindow(w.hwnd, showMode)
w.update()
}
func (w *window) WriteClipboard(mime string, s []byte) {
@@ -993,3 +990,6 @@ func configForDPI(dpi int) unit.Metric {
func (Win32ViewEvent) implementsViewEvent() {}
func (Win32ViewEvent) ImplementsEvent() {}
func (w Win32ViewEvent) Valid() bool {
return w != (Win32ViewEvent{})
}
+10 -14
View File
@@ -113,9 +113,6 @@ type x11Window struct {
wakeups chan struct{}
handler x11EventHandler
buf [100]byte
// invMy avoids the race between destroy and Invalidate.
invMu sync.Mutex
}
var (
@@ -389,6 +386,7 @@ func (w *x11Window) ProcessEvent(e event.Event) {
func (w *x11Window) shutdown(err error) {
w.ProcessEvent(X11ViewEvent{})
w.ProcessEvent(DestroyEvent{Err: err})
w.destroy()
}
func (w *x11Window) Event() event.Event {
@@ -398,9 +396,6 @@ func (w *x11Window) Event() event.Event {
w.dispatch()
continue
}
if _, destroy := evt.(DestroyEvent); destroy {
w.destroy()
}
return evt
}
}
@@ -418,11 +413,6 @@ func (w *x11Window) Invalidate() {
case w.wakeups <- struct{}{}:
default:
}
w.invMu.Lock()
defer w.invMu.Unlock()
if w.x == nil {
return
}
if _, err := syscall.Write(w.notify.write, x11OneByte); err != nil && err != syscall.EAGAIN {
panic(fmt.Errorf("failed to write to pipe: %v", err))
}
@@ -464,7 +454,12 @@ func (w *x11Window) dispatch() {
// Check for pending draw events before checking animation or blocking.
// This fixes an issue on Xephyr where on startup XPending() > 0 but
// poll will still block. This also prevents no-op calls to poll.
if syn = w.handler.handleEvents(); !syn {
syn = w.handler.handleEvents()
if w.x == nil {
// handleEvents received a close request and destroyed the window.
return
}
if !syn {
anim = w.animating
if !anim {
// Clear poll events.
@@ -476,6 +471,9 @@ func (w *x11Window) dispatch() {
switch {
case *xEvents&syscall.POLLIN != 0:
syn = w.handler.handleEvents()
if w.x == nil {
return
}
case *xEvents&(syscall.POLLERR|syscall.POLLHUP) != 0:
}
}
@@ -503,8 +501,6 @@ func (w *x11Window) dispatch() {
}
func (w *x11Window) destroy() {
w.invMu.Lock()
defer w.invMu.Unlock()
if w.notify.write != 0 {
syscall.Close(w.notify.write)
w.notify.write = 0
+1 -1
View File
@@ -175,7 +175,7 @@ func (c *vkContext) refresh(surf vk.Surface, width, height int) error {
if err != nil {
return err
}
minExt, maxExt := caps.MinExtent(), caps.MaxExtent()
minExt, maxExt := vk.SurfaceCapabilitiesMinExtent(caps), vk.SurfaceCapabilitiesMaxExtent(caps)
if width < minExt.X || maxExt.X < width || height < minExt.Y || maxExt.Y < height {
return errOutOfDate
}
+130 -71
View File
@@ -7,7 +7,6 @@ import (
"fmt"
"image"
"image/color"
"reflect"
"runtime"
"sync"
"time"
@@ -36,14 +35,15 @@ type Option func(unit.Metric, *Config)
// Window represents an operating system window.
//
// The zero-value Window is useful, and calling any method on
// it creates and shows a new GUI window. On iOS or Android,
// the first Window represents the the window previously
// created by the platform.
// The zero-value Window is useful; the GUI window is created and shown the first
// time the [Event] method is called. On iOS or Android, the first Window represents
// the window previously created by the platform.
//
// More than one Window is not supported on iOS, Android,
// WebAssembly.
// More than one Window is not supported on iOS, Android, WebAssembly.
type Window struct {
initialOpts []Option
initialActions []system.Action
ctx context
gpu gpu.GPU
// timer tracks the delayed invalidate goroutine.
@@ -89,13 +89,18 @@ type Window struct {
}
imeState editorState
driver driver
// basic is the driver interface that is needed even after the window is gone.
basic basicDriver
once sync.Once
// gpuErr tracks the GPU error that is to be reported when
// the window is closed.
gpuErr error
// invMu protects mayInvalidate.
invMu sync.Mutex
mayInvalidate bool
// coalesced tracks the most recent events waiting to be delivered
// to the client.
coalesced eventSummary
// frame tracks the most recently frame event.
// frame tracks the most recent frame event.
lastFrame struct {
sync bool
size image.Point
@@ -105,11 +110,12 @@ type Window struct {
}
type eventSummary struct {
wakeup bool
cfg *ConfigEvent
view *ViewEvent
frame *frameEvent
destroy *DestroyEvent
wakeup bool
cfg *ConfigEvent
view *ViewEvent
frame *frameEvent
framePending bool
destroy *DestroyEvent
}
type callbacks struct {
@@ -216,6 +222,7 @@ func (w *Window) frame(frame *op.Ops, viewport image.Point) error {
}
func (w *Window) processFrame(frame *op.Ops, ack chan<- struct{}) {
w.coalesced.framePending = false
wrapper := &w.decorations.Ops
off := op.Offset(w.lastFrame.off).Push(wrapper)
ops.AddCall(&wrapper.Internal, &frame.Internal, ops.PC{}, ops.PCFor(&frame.Internal))
@@ -223,7 +230,8 @@ func (w *Window) processFrame(frame *op.Ops, ack chan<- struct{}) {
w.lastFrame.deco.Add(wrapper)
if err := w.validateAndProcess(w.lastFrame.size, w.lastFrame.sync, wrapper, ack); err != nil {
w.destroyGPU()
w.driver.ProcessEvent(DestroyEvent{Err: err})
w.gpuErr = err
w.driver.Perform(system.ActionClose)
return
}
w.updateState()
@@ -273,8 +281,12 @@ func (w *Window) updateState() {
//
// Invalidate is safe for concurrent use.
func (w *Window) Invalidate() {
w.init()
w.basic.Invalidate()
w.invMu.Lock()
defer w.invMu.Unlock()
if w.mayInvalidate {
w.mayInvalidate = false
w.driver.Invalidate()
}
}
// Option applies the options to the window. The options are hints; the platform is
@@ -283,7 +295,10 @@ func (w *Window) Option(opts ...Option) {
if len(opts) == 0 {
return
}
w.init(opts...)
if w.driver == nil {
w.initialOpts = append(w.initialOpts, opts...)
return
}
w.Run(func() {
cnf := Config{Decorated: w.decorations.enabled}
for _, opt := range opts {
@@ -302,13 +317,14 @@ func (w *Window) Option(opts ...Option) {
}
// Run f in the same thread as the native window event loop, and wait for f to
// return or the window to close.
// return or the window to close. If the window has not yet been created,
// Run calls f directly.
//
// Note that most programs should not call Run; configuring a Window with
// [CustomRenderer] is a notable exception.
func (w *Window) Run(f func()) {
w.init()
if w.driver == nil {
f()
return
}
done := make(chan struct{})
@@ -374,11 +390,13 @@ func (w *Window) setNextFrame(at time.Time) {
}
}
func (c *callbacks) SetDriver(d basicDriver) {
c.w.basic = d
if d, ok := d.(driver); ok {
c.w.driver = d
func (c *callbacks) SetDriver(d driver) {
if d == nil {
panic("nil driver")
}
c.w.invMu.Lock()
defer c.w.invMu.Unlock()
c.w.driver = d
}
func (c *callbacks) ProcessFrame(frame *op.Ops, ack chan<- struct{}) {
@@ -545,10 +563,20 @@ func (c *callbacks) Invalidate() {
}
func (c *callbacks) nextEvent() (event.Event, bool) {
s := &c.w.coalesced
// Every event counts as a wakeup.
defer func() { s.wakeup = false }()
return c.w.nextEvent()
}
func (w *Window) nextEvent() (event.Event, bool) {
s := &w.coalesced
defer func() {
// Every event counts as a wakeup.
s.wakeup = false
}()
switch {
case s.framePending:
// If the user didn't call FrameEvent.Event, process
// an empty frame.
w.processFrame(new(op.Ops), nil)
case s.view != nil:
e := *s.view
s.view = nil
@@ -565,10 +593,14 @@ func (c *callbacks) nextEvent() (event.Event, bool) {
case s.frame != nil:
e := *s.frame
s.frame = nil
s.framePending = true
return e.FrameEvent, true
case s.wakeup:
return wakeupEvent{}, true
}
w.invMu.Lock()
defer w.invMu.Unlock()
w.mayInvalidate = w.driver != nil
return nil, false
}
@@ -611,15 +643,21 @@ func (w *Window) processEvent(e event.Event) bool {
e2.Size = e2.Size.Sub(offset)
w.coalesced.frame = &e2
case DestroyEvent:
if w.gpuErr != nil {
e2.Err = w.gpuErr
}
w.destroyGPU()
w.invMu.Lock()
w.mayInvalidate = false
w.driver = nil
w.invMu.Unlock()
if q := w.timer.quit; q != nil {
q <- struct{}{}
<-q
}
w.coalesced.destroy = &e2
case ViewEvent:
if reflect.ValueOf(e2).IsZero() && w.gpu != nil {
if !e2.Valid() && w.gpu != nil {
w.ctx.Lock()
w.gpu.Release()
w.gpu = nil
@@ -627,6 +665,7 @@ func (w *Window) processEvent(e event.Event) bool {
}
w.coalesced.view = &e2
case ConfigEvent:
w.decorations.Decorations.Maximized = e2.Config.Mode == Maximized
wasFocused := w.decorations.Config.Focused
w.decorations.Config = e2.Config
e2.Config = w.effectiveConfig()
@@ -680,48 +719,61 @@ func (w *Window) processEvent(e event.Event) bool {
}
// Event blocks until an event is received from the window, such as
// [FrameEvent], or until [Invalidate] is called.
// [FrameEvent], or until [Invalidate] is called. The window is created
// and shown the first time Event is called.
func (w *Window) Event() event.Event {
w.init()
return w.basic.Event()
if w.driver == nil {
w.init()
}
if w.driver == nil {
e, ok := w.nextEvent()
if !ok {
panic("window initialization failed without a DestroyEvent")
}
return e
}
return w.driver.Event()
}
func (w *Window) init(initial ...Option) {
w.once.Do(func() {
debug.Parse()
// Measure decoration height.
deco := new(widget.Decorations)
theme := material.NewTheme()
theme.Shaper = text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Regular()))
decoStyle := material.Decorations(theme, deco, 0, "")
gtx := layout.Context{
Ops: new(op.Ops),
// Measure in Dp.
Metric: unit.Metric{},
}
// Allow plenty of space.
gtx.Constraints.Max.Y = 200
dims := decoStyle.Layout(gtx)
decoHeight := unit.Dp(dims.Size.Y)
defaultOptions := []Option{
Size(800, 600),
Title("Gio"),
Decorated(true),
decoHeightOpt(decoHeight),
}
options := append(defaultOptions, initial...)
var cnf Config
cnf.apply(unit.Metric{}, options)
func (w *Window) init() {
debug.Parse()
// Measure decoration height.
deco := new(widget.Decorations)
theme := material.NewTheme()
theme.Shaper = text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Regular()))
decoStyle := material.Decorations(theme, deco, 0, "")
gtx := layout.Context{
Ops: new(op.Ops),
// Measure in Dp.
Metric: unit.Metric{},
}
// Allow plenty of space.
gtx.Constraints.Max.Y = 200
dims := decoStyle.Layout(gtx)
decoHeight := unit.Dp(dims.Size.Y)
defaultOptions := []Option{
Size(800, 600),
Title("Gio"),
Decorated(true),
decoHeightOpt(decoHeight),
}
options := append(defaultOptions, w.initialOpts...)
w.initialOpts = nil
var cnf Config
cnf.apply(unit.Metric{}, options)
w.nocontext = cnf.CustomRenderer
w.decorations.Theme = theme
w.decorations.Decorations = deco
w.decorations.enabled = cnf.Decorated
w.decorations.height = decoHeight
w.imeState.compose = key.Range{Start: -1, End: -1}
w.semantic.ids = make(map[input.SemanticID]input.SemanticNode)
newWindow(&callbacks{w}, options)
})
w.nocontext = cnf.CustomRenderer
w.decorations.Theme = theme
w.decorations.Decorations = deco
w.decorations.enabled = cnf.Decorated
w.decorations.height = decoHeight
w.imeState.compose = key.Range{Start: -1, End: -1}
w.semantic.ids = make(map[input.SemanticID]input.SemanticNode)
newWindow(&callbacks{w}, options)
for _, acts := range w.initialActions {
w.Perform(acts)
}
w.initialActions = nil
}
func (w *Window) updateCursor() {
@@ -759,7 +811,6 @@ func (w *Window) decorate(e FrameEvent, o *op.Ops) image.Point {
default:
panic(fmt.Errorf("unknown WindowMode %v", m))
}
deco.Perform(actions)
gtx := layout.Context{
Ops: o,
Now: e.Now,
@@ -769,8 +820,12 @@ func (w *Window) decorate(e FrameEvent, o *op.Ops) image.Point {
}
// Update the window based on the actions on the decorations.
opts, acts := splitActions(deco.Update(gtx))
w.driver.Configure(opts)
w.driver.Perform(acts)
if len(opts) > 0 {
w.driver.Configure(opts)
}
if acts != 0 {
w.driver.Perform(acts)
}
style.Layout(gtx)
// Offset to place the frame content below the decorations.
decoHeight := gtx.Dp(w.decorations.Config.decoHeight)
@@ -817,6 +872,10 @@ func (w *Window) Perform(actions system.Action) {
if acts == 0 {
return
}
if w.driver == nil {
w.initialActions = append(w.initialActions, acts)
return
}
w.Run(func() {
w.driver.Perform(actions)
})
Generated
+15 -31
View File
@@ -9,11 +9,11 @@
]
},
"locked": {
"lastModified": 1701721028,
"narHash": "sha256-2z4YrdHPLoMZNWR1MPOjNZMqPg057i1eZXaYI6RTahQ=",
"lastModified": 1733430059,
"narHash": "sha256-o3O5tjrMMebRLuHQt7BbEw3jZgWRW5vnOptNXv8WdO4=",
"owner": "tadfisher",
"repo": "android-nixpkgs",
"rev": "c923f9ec0f4dd0d7dc725dc5b73fbf03658e50dd",
"rev": "d2f3c1ea99c0bea9d28a0e59daeb482f50d4cd35",
"type": "github"
},
"original": {
@@ -27,15 +27,14 @@
"nixpkgs": [
"android",
"nixpkgs"
],
"systems": "systems"
]
},
"locked": {
"lastModified": 1701697687,
"narHash": "sha256-dLLE5wQBVv+pIb4bWmKFSw2DvLVyuEk0F7ng6hpZPSU=",
"lastModified": 1728330715,
"narHash": "sha256-xRJ2nPOXb//u1jaBnDP56M7v5ldavjbtR6lfGqSvcKg=",
"owner": "numtide",
"repo": "devshell",
"rev": "c3bd77911391eb1638af6ce773de86da57ee6df5",
"rev": "dd6b80932022cea34a019e2bb32f6fa9e494dfef",
"type": "github"
},
"original": {
@@ -46,14 +45,14 @@
},
"flake-utils": {
"inputs": {
"systems": "systems_2"
"systems": "systems"
},
"locked": {
"lastModified": 1701680307,
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
@@ -64,16 +63,16 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1701282334,
"narHash": "sha256-MxCVrXY6v4QmfTwIysjjaX0XUhqBbxTWWB4HXtDYsdk=",
"lastModified": 1733261153,
"narHash": "sha256-eq51hyiaIwtWo19fPEeE0Zr2s83DYMKJoukNLgGGpek=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "057f9aecfb71c4437d2b27d3323df7f93c010b7e",
"rev": "b681065d0919f7eb5309a93cea2cfa84dec9aa88",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "23.11",
"ref": "nixos-24.11",
"repo": "nixpkgs",
"type": "github"
}
@@ -98,21 +97,6 @@
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
+1 -7
View File
@@ -3,7 +3,7 @@
description = "Gio build environment";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/23.11";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
android.url = "github:tadfisher/android-nixpkgs";
android.inputs.nixpkgs.follows = "nixpkgs";
};
@@ -47,12 +47,6 @@
xorg.libXfixes
libGL
pkg-config
] else if stdenv.isDarwin then [
darwin.apple_sdk_11_0.frameworks.Foundation
darwin.apple_sdk_11_0.frameworks.Metal
darwin.apple_sdk_11_0.frameworks.QuartzCore
darwin.apple_sdk_11_0.frameworks.AppKit
darwin.apple_sdk_11_0.MacOSX-SDK
] else [ ]);
} // (if stdenv.isLinux then {
LD_LIBRARY_PATH = "${vulkan-loader}/lib";
+1 -1
View File
@@ -34,7 +34,7 @@ type Font struct {
// Face is an opaque handle to a typeface. The concrete implementation depends
// upon the kind of font and shaper in use.
type Face interface {
Face() font.Face
Face() *font.Face
}
// Typeface identifies a list of font families to attempt to use for displaying
+41 -43
View File
@@ -16,23 +16,21 @@ import (
_ "image/png"
giofont "gioui.org/font"
"github.com/go-text/typesetting/font"
fontapi "github.com/go-text/typesetting/opentype/api/font"
"github.com/go-text/typesetting/opentype/api/metadata"
"github.com/go-text/typesetting/opentype/loader"
fontapi "github.com/go-text/typesetting/font"
"github.com/go-text/typesetting/font/opentype"
)
// Face is a thread-safe representation of a loaded font. For efficiency, applications
// should construct a face for any given font file once, reusing it across different
// text shapers.
type Face struct {
face font.Font
face *fontapi.Font
font giofont.Font
}
// Parse constructs a Face from source bytes.
func Parse(src []byte) (Face, error) {
ld, err := loader.NewLoader(bytes.NewReader(src))
ld, err := opentype.NewLoader(bytes.NewReader(src))
if err != nil {
return Face{}, err
}
@@ -49,11 +47,11 @@ func Parse(src []byte) (Face, error) {
// ParseCollection parse an Opentype font file, with support for collections.
// Single font files are supported, returning a slice with length 1.
// The returned fonts are automatically wrapped in a text.FontFace with
// inferred font metadata.
// inferred font font.
// BUG(whereswaldon): the only Variant that can be detected automatically is
// "Mono".
func ParseCollection(src []byte) ([]giofont.FontFace, error) {
lds, err := loader.NewLoaders(bytes.NewReader(src))
lds, err := opentype.NewLoaders(bytes.NewReader(src))
if err != nil {
return nil, err
}
@@ -76,7 +74,7 @@ func ParseCollection(src []byte) ([]giofont.FontFace, error) {
return out, nil
}
func DescriptionToFont(md metadata.Description) giofont.Font {
func DescriptionToFont(md fontapi.Description) giofont.Font {
return giofont.Font{
Typeface: giofont.Typeface(md.Family),
Style: gioStyle(md.Aspect.Style),
@@ -84,30 +82,30 @@ func DescriptionToFont(md metadata.Description) giofont.Font {
}
}
func FontToDescription(font giofont.Font) metadata.Description {
return metadata.Description{
func FontToDescription(font giofont.Font) fontapi.Description {
return fontapi.Description{
Family: string(font.Typeface),
Aspect: metadata.Aspect{
Aspect: fontapi.Aspect{
Style: mdStyle(font.Style),
Weight: mdWeight(font.Weight),
},
}
}
// parseLoader parses the contents of the loader into a face and its metadata.
func parseLoader(ld *loader.Loader) (font.Font, giofont.Font, error) {
// parseLoader parses the contents of the loader into a face and its font.
func parseLoader(ld *opentype.Loader) (*fontapi.Font, giofont.Font, error) {
ft, err := fontapi.NewFont(ld)
if err != nil {
return nil, giofont.Font{}, err
}
data := DescriptionToFont(metadata.Metadata(ld))
data := DescriptionToFont(ft.Describe())
return ft, data, nil
}
// Face returns a thread-unsafe wrapper for this Face suitable for use by a single shaper.
// Face many be invoked any number of times and is safe so long as each return value is
// only used by one goroutine.
func (f Face) Face() font.Face {
func (f Face) Face() *fontapi.Face {
return &fontapi.Face{Font: f.face}
}
@@ -119,74 +117,74 @@ func (f Face) Font() giofont.Font {
return f.font
}
func gioStyle(s metadata.Style) giofont.Style {
func gioStyle(s fontapi.Style) giofont.Style {
switch s {
case metadata.StyleItalic:
case fontapi.StyleItalic:
return giofont.Italic
case metadata.StyleNormal:
case fontapi.StyleNormal:
fallthrough
default:
return giofont.Regular
}
}
func mdStyle(g giofont.Style) metadata.Style {
func mdStyle(g giofont.Style) fontapi.Style {
switch g {
case giofont.Italic:
return metadata.StyleItalic
return fontapi.StyleItalic
case giofont.Regular:
fallthrough
default:
return metadata.StyleNormal
return fontapi.StyleNormal
}
}
func gioWeight(w metadata.Weight) giofont.Weight {
func gioWeight(w fontapi.Weight) giofont.Weight {
switch w {
case metadata.WeightThin:
case fontapi.WeightThin:
return giofont.Thin
case metadata.WeightExtraLight:
case fontapi.WeightExtraLight:
return giofont.ExtraLight
case metadata.WeightLight:
case fontapi.WeightLight:
return giofont.Light
case metadata.WeightNormal:
case fontapi.WeightNormal:
return giofont.Normal
case metadata.WeightMedium:
case fontapi.WeightMedium:
return giofont.Medium
case metadata.WeightSemibold:
case fontapi.WeightSemibold:
return giofont.SemiBold
case metadata.WeightBold:
case fontapi.WeightBold:
return giofont.Bold
case metadata.WeightExtraBold:
case fontapi.WeightExtraBold:
return giofont.ExtraBold
case metadata.WeightBlack:
case fontapi.WeightBlack:
return giofont.Black
default:
return giofont.Normal
}
}
func mdWeight(g giofont.Weight) metadata.Weight {
func mdWeight(g giofont.Weight) fontapi.Weight {
switch g {
case giofont.Thin:
return metadata.WeightThin
return fontapi.WeightThin
case giofont.ExtraLight:
return metadata.WeightExtraLight
return fontapi.WeightExtraLight
case giofont.Light:
return metadata.WeightLight
return fontapi.WeightLight
case giofont.Normal:
return metadata.WeightNormal
return fontapi.WeightNormal
case giofont.Medium:
return metadata.WeightMedium
return fontapi.WeightMedium
case giofont.SemiBold:
return metadata.WeightSemibold
return fontapi.WeightSemibold
case giofont.Bold:
return metadata.WeightBold
return fontapi.WeightBold
case giofont.ExtraBold:
return metadata.WeightExtraBold
return fontapi.WeightExtraBold
case giofont.Black:
return metadata.WeightBlack
return fontapi.WeightBlack
default:
return metadata.WeightNormal
return fontapi.WeightNormal
}
}
+5 -4
View File
@@ -271,12 +271,13 @@ func (s *Scroll) Stop() {
}
// Update state and report the scroll distance along axis.
func (s *Scroll) Update(cfg unit.Metric, q input.Source, t time.Time, axis Axis, bounds image.Rectangle) int {
func (s *Scroll) Update(cfg unit.Metric, q input.Source, t time.Time, axis Axis, scrollx, scrolly pointer.ScrollRange) int {
total := 0
f := pointer.Filter{
Target: s,
Kinds: pointer.Press | pointer.Drag | pointer.Release | pointer.Scroll | pointer.Cancel,
ScrollBounds: bounds,
Target: s,
Kinds: pointer.Press | pointer.Drag | pointer.Release | pointer.Scroll | pointer.Cancel,
ScrollX: scrollx,
ScrollY: scrolly,
}
for {
evt, ok := q.Event(f)
+7 -9
View File
@@ -1,16 +1,14 @@
module gioui.org
go 1.19
go 1.21
require (
eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2
gioui.org/shader v1.0.8
github.com/go-text/typesetting v0.1.1
golang.org/x/exp v0.0.0-20221012211006-4de253d81b95
golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91
golang.org/x/image v0.5.0
golang.org/x/sys v0.5.0
github.com/go-text/typesetting v0.2.1
golang.org/x/exp v0.0.0-20240707233637-46b078467d37
golang.org/x/exp/shiny v0.0.0-20240707233637-46b078467d37
golang.org/x/image v0.18.0
golang.org/x/sys v0.22.0
golang.org/x/text v0.16.0
)
require golang.org/x/text v0.9.0
+14 -39
View File
@@ -1,44 +1,19 @@
eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d h1:ARo7NCVvN2NdhLlJE9xAbKweuI9L6UgfTbYb0YwPacY=
eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d/go.mod h1:OYVuxibdk9OSLX8vAqydtRPP87PyTFcT9uH3MlEGBQA=
gioui.org/cpu v0.0.0-20210808092351-bfe733dd3334/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 h1:AGDDxsJE1RpcXTAxPG2B4jrwVUJGFDjINIPi1jtO6pc=
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=
gioui.org/shader v1.0.8 h1:6ks0o/A+b0ne7RzEqRZK5f4Gboz2CfG+mVliciy6+qA=
gioui.org/shader v1.0.8/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM=
github.com/go-text/typesetting v0.1.1 h1:bGAesCuo85nXnEN5LmFMVGAGpGkCPtHrZLi//qD7EJo=
github.com/go-text/typesetting v0.1.1/go.mod h1:d22AnmeKq/on0HNv73UFriMKc4Ez6EqZAofLhAzpSzI=
github.com/go-text/typesetting-utils v0.0.0-20231211103740-d9332ae51f04 h1:zBx+p/W2aQYtNuyZNcTfinWvXBQwYtDfme051PR/lAY=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20221012211006-4de253d81b95 h1:sBdrWpxhGDdTAYNqbgBLAR+ULAPPhfgncLr1X0lyWtg=
golang.org/x/exp v0.0.0-20221012211006-4de253d81b95/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91 h1:ryT6Nf0R83ZgD8WnFFdfI8wCeyqgdXWN4+CkFVNPAT0=
golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91/go.mod h1:VjAR7z0ngyATZTELrBSkxOOHhhlnVUxDye4mcjx5h/8=
golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI=
golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
github.com/go-text/typesetting v0.2.1 h1:x0jMOGyO3d1qFAPI0j4GSsh7M0Q3Ypjzr4+CEVg82V8=
github.com/go-text/typesetting v0.2.1/go.mod h1:mTOxEwasOFpAMBjEQDhdWRckoLLeI/+qrQeBCTGEt6M=
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0=
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
golang.org/x/exp v0.0.0-20240707233637-46b078467d37 h1:uLDX+AfeFCct3a2C7uIWBKMJIR3CJMhcgfrUAqjRK6w=
golang.org/x/exp v0.0.0-20240707233637-46b078467d37/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/exp/shiny v0.0.0-20240707233637-46b078467d37 h1:SOSg7+sueresE4IbmmGM60GmlIys+zNX63d6/J4CMtU=
golang.org/x/exp/shiny v0.0.0-20240707233637-46b078467d37/go.mod h1:3F+MieQB7dRYLTmnncoFbb1crS5lfQoTfDgQy6K4N0o=
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
-2193
View File
File diff suppressed because it is too large Load Diff
-129
View File
@@ -1,129 +0,0 @@
// SPDX-License-Identifier: Unlicense OR MIT
package gpu
import (
"unsafe"
"gioui.org/cpu"
)
// This file contains code specific to running compute shaders on the CPU.
// dispatcher dispatches CPU compute programs across multiple goroutines.
type dispatcher struct {
// done is notified when a worker completes its work slice.
done chan struct{}
// work receives work slice indices. It is closed when the dispatcher is released.
work chan work
// dispatch receives compute jobs, which is then split among workers.
dispatch chan dispatch
// sync receives notification when a Sync completes.
sync chan struct{}
}
type work struct {
ctx *cpu.DispatchContext
index int
}
type dispatch struct {
_type jobType
program *cpu.ProgramInfo
descSet unsafe.Pointer
x, y, z int
}
type jobType uint8
const (
jobDispatch jobType = iota
jobBarrier
jobSync
)
func newDispatcher(workers int) *dispatcher {
d := &dispatcher{
work: make(chan work, workers),
done: make(chan struct{}, workers),
// Leave some room to avoid blocking calls to Dispatch.
dispatch: make(chan dispatch, 20),
sync: make(chan struct{}),
}
for i := 0; i < workers; i++ {
go d.worker()
}
go d.dispatcher()
return d
}
func (d *dispatcher) dispatcher() {
defer close(d.work)
var free []*cpu.DispatchContext
defer func() {
for _, ctx := range free {
ctx.Free()
}
}()
var used []*cpu.DispatchContext
for job := range d.dispatch {
switch job._type {
case jobDispatch:
if len(free) == 0 {
free = append(free, cpu.NewDispatchContext())
}
ctx := free[len(free)-1]
free = free[:len(free)-1]
used = append(used, ctx)
ctx.Prepare(cap(d.work), job.program, job.descSet, job.x, job.y, job.z)
for i := 0; i < cap(d.work); i++ {
d.work <- work{
ctx: ctx,
index: i,
}
}
case jobBarrier:
// Wait for all outstanding dispatches to complete.
for i := 0; i < len(used)*cap(d.work); i++ {
<-d.done
}
free = append(free, used...)
used = used[:0]
case jobSync:
d.sync <- struct{}{}
}
}
}
func (d *dispatcher) worker() {
thread := cpu.NewThreadContext()
defer thread.Free()
for w := range d.work {
w.ctx.Dispatch(w.index, thread)
d.done <- struct{}{}
}
}
func (d *dispatcher) Barrier() {
d.dispatch <- dispatch{_type: jobBarrier}
}
func (d *dispatcher) Sync() {
d.dispatch <- dispatch{_type: jobSync}
<-d.sync
}
func (d *dispatcher) Dispatch(program *cpu.ProgramInfo, descSet unsafe.Pointer, x, y, z int) {
d.dispatch <- dispatch{
_type: jobDispatch,
program: program,
descSet: descSet,
x: x,
y: y,
z: z,
}
}
func (d *dispatcher) Stop() {
close(d.dispatch)
}
+115 -94
View File
@@ -9,11 +9,11 @@ package gpu
import (
"encoding/binary"
"errors"
"fmt"
"image"
"image/color"
"math"
"os"
"reflect"
"time"
"unsafe"
@@ -261,7 +261,7 @@ type texture struct {
type blitter struct {
ctx driver.Device
viewport image.Point
pipelines [3]*pipeline
pipelines [2][3]*pipeline
colUniforms *blitColUniforms
texUniforms *blitTexUniforms
linearGradientUniforms *blitLinearGradientUniforms
@@ -343,13 +343,12 @@ func New(api API) (GPU, error) {
func NewWithDevice(d driver.Device) (GPU, error) {
d.BeginFrame(nil, false, image.Point{})
defer d.EndFrame()
forceCompute := os.Getenv("GIORENDERER") == "forcecompute"
feats := d.Caps().Features
switch {
case !forceCompute && feats.Has(driver.FeatureFloatRenderTargets) && feats.Has(driver.FeatureSRGB):
case feats.Has(driver.FeatureFloatRenderTargets) && feats.Has(driver.FeatureSRGB):
return newGPU(d)
}
return newCompute(d)
return nil, errors.New("no available GPU driver")
}
func newGPU(ctx driver.Device) (*gpu, error) {
@@ -560,12 +559,24 @@ func newBlitter(ctx driver.Device) *blitter {
func (b *blitter) release() {
b.quadVerts.Release()
for _, p := range b.pipelines {
p.Release()
for _, p := range p {
p.Release()
}
}
}
func createColorPrograms(b driver.Device, vsSrc shader.Sources, fsSrc [3]shader.Sources, uniforms [3]interface{}) ([3]*pipeline, error) {
var pipelines [3]*pipeline
func createColorPrograms(b driver.Device, vsSrc shader.Sources, fsSrc [3]shader.Sources, uniforms [3]interface{}) (pipelines [2][3]*pipeline, err error) {
defer func() {
if err != nil {
for _, p := range pipelines {
for _, p := range p {
if p != nil {
p.Release()
}
}
}
}
}()
blend := driver.BlendDesc{
Enable: true,
SrcFactor: driver.BlendFactorOne,
@@ -583,86 +594,76 @@ func createColorPrograms(b driver.Device, vsSrc shader.Sources, fsSrc [3]shader.
return pipelines, err
}
defer vsh.Release()
{
fsh, err := b.NewFragmentShader(fsSrc[materialTexture])
if err != nil {
return pipelines, err
for i, format := range []driver.TextureFormat{driver.TextureFormatOutput, driver.TextureFormatSRGBA} {
{
fsh, err := b.NewFragmentShader(fsSrc[materialTexture])
if err != nil {
return pipelines, err
}
defer fsh.Release()
pipe, err := b.NewPipeline(driver.PipelineDesc{
VertexShader: vsh,
FragmentShader: fsh,
BlendDesc: blend,
VertexLayout: layout,
PixelFormat: format,
Topology: driver.TopologyTriangleStrip,
})
if err != nil {
return pipelines, err
}
var vertBuffer *uniformBuffer
if u := uniforms[materialTexture]; u != nil {
vertBuffer = newUniformBuffer(b, u)
}
pipelines[i][materialTexture] = &pipeline{pipe, vertBuffer}
}
defer fsh.Release()
pipe, err := b.NewPipeline(driver.PipelineDesc{
VertexShader: vsh,
FragmentShader: fsh,
BlendDesc: blend,
VertexLayout: layout,
PixelFormat: driver.TextureFormatOutput,
Topology: driver.TopologyTriangleStrip,
})
if err != nil {
return pipelines, err
{
var vertBuffer *uniformBuffer
fsh, err := b.NewFragmentShader(fsSrc[materialColor])
if err != nil {
return pipelines, err
}
defer fsh.Release()
pipe, err := b.NewPipeline(driver.PipelineDesc{
VertexShader: vsh,
FragmentShader: fsh,
BlendDesc: blend,
VertexLayout: layout,
PixelFormat: format,
Topology: driver.TopologyTriangleStrip,
})
if err != nil {
return pipelines, err
}
if u := uniforms[materialColor]; u != nil {
vertBuffer = newUniformBuffer(b, u)
}
pipelines[i][materialColor] = &pipeline{pipe, vertBuffer}
}
var vertBuffer *uniformBuffer
if u := uniforms[materialTexture]; u != nil {
vertBuffer = newUniformBuffer(b, u)
{
var vertBuffer *uniformBuffer
fsh, err := b.NewFragmentShader(fsSrc[materialLinearGradient])
if err != nil {
return pipelines, err
}
defer fsh.Release()
pipe, err := b.NewPipeline(driver.PipelineDesc{
VertexShader: vsh,
FragmentShader: fsh,
BlendDesc: blend,
VertexLayout: layout,
PixelFormat: format,
Topology: driver.TopologyTriangleStrip,
})
if err != nil {
return pipelines, err
}
if u := uniforms[materialLinearGradient]; u != nil {
vertBuffer = newUniformBuffer(b, u)
}
pipelines[i][materialLinearGradient] = &pipeline{pipe, vertBuffer}
}
pipelines[materialTexture] = &pipeline{pipe, vertBuffer}
}
{
var vertBuffer *uniformBuffer
fsh, err := b.NewFragmentShader(fsSrc[materialColor])
if err != nil {
pipelines[materialTexture].Release()
return pipelines, err
}
defer fsh.Release()
pipe, err := b.NewPipeline(driver.PipelineDesc{
VertexShader: vsh,
FragmentShader: fsh,
BlendDesc: blend,
VertexLayout: layout,
PixelFormat: driver.TextureFormatOutput,
Topology: driver.TopologyTriangleStrip,
})
if err != nil {
pipelines[materialTexture].Release()
return pipelines, err
}
if u := uniforms[materialColor]; u != nil {
vertBuffer = newUniformBuffer(b, u)
}
pipelines[materialColor] = &pipeline{pipe, vertBuffer}
}
{
var vertBuffer *uniformBuffer
fsh, err := b.NewFragmentShader(fsSrc[materialLinearGradient])
if err != nil {
pipelines[materialTexture].Release()
pipelines[materialColor].Release()
return pipelines, err
}
defer fsh.Release()
pipe, err := b.NewPipeline(driver.PipelineDesc{
VertexShader: vsh,
FragmentShader: fsh,
BlendDesc: blend,
VertexLayout: layout,
PixelFormat: driver.TextureFormatOutput,
Topology: driver.TopologyTriangleStrip,
})
if err != nil {
pipelines[materialTexture].Release()
pipelines[materialColor].Release()
return pipelines, err
}
if u := uniforms[materialLinearGradient]; u != nil {
vertBuffer = newUniformBuffer(b, u)
}
pipelines[materialLinearGradient] = &pipeline{pipe, vertBuffer}
}
if err != nil {
for _, p := range pipelines {
p.Release()
}
return pipelines, err
}
return pipelines, nil
}
@@ -865,7 +866,7 @@ func (r *renderer) drawLayers(layers []opacityLayer, ops []imageOp) {
Min: l.place.Pos,
Max: l.place.Pos.Add(l.clip.Size()),
}
r.ctx.Viewport(v.Min.X, v.Min.Y, v.Max.X, v.Max.Y)
r.ctx.Viewport(v.Min.X, v.Min.Y, v.Dx(), v.Dy())
f := r.layerFBOs.fbos[fbo]
r.drawOps(true, l.clip.Min.Mul(-1), l.clip.Size(), ops[l.opStart:l.opEnd])
sr := f32.FRect(v)
@@ -930,7 +931,7 @@ func (d *drawOps) newPathOp() *pathOp {
return &d.pathOpCache[len(d.pathOpCache)-1]
}
func (d *drawOps) addClipPath(state *drawState, aux []byte, auxKey opKey, bounds f32.Rectangle, off f32.Point, push bool) {
func (d *drawOps) addClipPath(state *drawState, aux []byte, auxKey opKey, bounds f32.Rectangle, off f32.Point) {
npath := d.newPathOp()
*npath = pathOp{
parent: state.cpath,
@@ -1055,7 +1056,7 @@ loop:
quads.aux, bounds, _ = d.boundsForTransformedRect(bounds, trans)
quads.key = opKey{Key: encOp.Key}
}
d.addClipPath(&state, quads.aux, quads.key, bounds, off, true)
d.addClipPath(&state, quads.aux, quads.key, bounds, off)
quads = quadsOp{}
case ops.TypePopClip:
state.cpath = state.cpath.parent
@@ -1100,7 +1101,7 @@ loop:
// this transformed rectangle.
k := opKey{Key: encOp.Key}
k.SetTransform(t) // TODO: This call has no effect.
d.addClipPath(&state, clipData, k, bnd, off, false)
d.addClipPath(&state, clipData, k, bnd, off)
}
bounds := cl.Round()
@@ -1230,7 +1231,7 @@ func (r *renderer) prepareDrawOps(ops []imageOp) {
}
}
func (r *renderer) drawOps(isFBO bool, opOff image.Point, viewport image.Point, ops []imageOp) {
func (r *renderer) drawOps(isFBO bool, opOff, viewport image.Point, ops []imageOp) {
var coverTex driver.Texture
for i := 0; i < len(ops); i++ {
img := ops[i]
@@ -1244,9 +1245,13 @@ func (r *renderer) drawOps(isFBO bool, opOff image.Point, viewport image.Point,
scale, off := clipSpaceTransform(drc, viewport)
var fbo FBO
fboIdx := 0
if isFBO {
fboIdx = 1
}
switch img.clipType {
case clipTypeNone:
p := r.blitter.pipelines[m.material]
p := r.blitter.pipelines[fboIdx][m.material]
r.ctx.BindPipeline(p.pipeline)
r.ctx.BindVertexBuffer(r.blitter.quadVerts, 0)
r.blitter.blit(m.material, isFBO, m.color, m.color1, m.color2, scale, off, m.opacity, m.uvTrans)
@@ -1265,7 +1270,7 @@ func (r *renderer) drawOps(isFBO bool, opOff image.Point, viewport image.Point,
Max: img.place.Pos.Add(drc.Size()),
}
coverScale, coverOff := texSpaceTransform(f32.FRect(uv), fbo.size)
p := r.pather.coverer.pipelines[m.material]
p := r.pather.coverer.pipelines[fboIdx][m.material]
r.ctx.BindPipeline(p.pipeline)
r.ctx.BindVertexBuffer(r.blitter.quadVerts, 0)
r.pather.cover(m.material, isFBO, m.color, m.color1, m.color2, scale, off, m.uvTrans, coverScale, coverOff)
@@ -1273,7 +1278,11 @@ func (r *renderer) drawOps(isFBO bool, opOff image.Point, viewport image.Point,
}
func (b *blitter) blit(mat materialType, fbo bool, col f32color.RGBA, col1, col2 f32color.RGBA, scale, off f32.Point, opacity float32, uvTrans f32.Affine2D) {
p := b.pipelines[mat]
fboIdx := 0
if fbo {
fboIdx = 1
}
p := b.pipelines[fboIdx][mat]
b.ctx.BindPipeline(p.pipeline)
var uniforms *blitUniforms
switch mat {
@@ -1474,7 +1483,7 @@ func (d *drawOps) buildVerts(pathData []byte, tr f32.Affine2D, outline bool, str
// as needed and feeds them to the supplied splitter.
func decodeToOutlineQuads(qs *quadSplitter, tr f32.Affine2D, pathData []byte) {
for len(pathData) >= scene.CommandSize+4 {
qs.contour = bo.Uint32(pathData)
qs.contour = binary.LittleEndian.Uint32(pathData)
cmd := ops.DecodeCommand(pathData[4:])
switch cmd.Op() {
case scene.OpLine:
@@ -1569,3 +1578,15 @@ func isPureOffset(t f32.Affine2D) bool {
a, b, _, d, e, _ := t.Elems()
return a == 1 && b == 0 && d == 0 && e == 1
}
func newShaders(ctx driver.Device, vsrc, fsrc shader.Sources) (vert driver.VertexShader, frag driver.FragmentShader, err error) {
vert, err = ctx.NewVertexShader(vsrc)
if err != nil {
return
}
frag, err = ctx.NewFragmentShader(fsrc)
if err != nil {
vert.Release()
}
return
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 334 B

After

Width:  |  Height:  |  Size: 1.9 KiB

+2 -2
View File
@@ -483,14 +483,14 @@ func TestGapsInPath(t *testing.T) {
func TestOpacity(t *testing.T) {
run(t, func(ops *op.Ops) {
opc1 := paint.PushOpacity(ops, .3)
// Fill screen to exercize the glClear optimization.
// Fill screen to exercise the glClear optimization.
paint.FillShape(ops, color.NRGBA{R: 255, A: 255}, clip.Rect{Max: image.Pt(1024, 1024)}.Op())
opc2 := paint.PushOpacity(ops, .6)
paint.FillShape(ops, color.NRGBA{G: 255, A: 255}, clip.Rect{Min: image.Pt(20, 10), Max: image.Pt(64, 128)}.Op())
opc2.Pop()
opc1.Pop()
opc3 := paint.PushOpacity(ops, .6)
paint.FillShape(ops, color.NRGBA{G: 255, A: 255}, clip.Rect{Min: image.Pt(50+20, 10), Max: image.Pt(50+64, 128)}.Op())
paint.FillShape(ops, color.NRGBA{B: 255, A: 255}, clip.Ellipse(image.Rectangle{Min: image.Pt(20+20, 10), Max: image.Pt(50+64, 128)}).Op(ops))
opc3.Pop()
}, func(r result) {
})
+2 -2
View File
@@ -862,8 +862,8 @@ func (b *Backend) BindUniforms(buffer driver.Buffer) {
buf := buffer.(*Buffer)
cmdBuf := b.currentCmdBuf()
for _, s := range b.pipe.pushRanges {
off := s.Offset()
vk.CmdPushConstants(cmdBuf, b.pipe.desc.layout, s.StageFlags(), off, buf.store[off:off+s.Size()])
off := vk.PushConstantRangeOffset(s)
vk.CmdPushConstants(cmdBuf, b.pipe.desc.layout, vk.PushConstantRangeStageFlags(s), off, buf.store[off:off+vk.PushConstantRangeSize(s)])
}
}
+9 -3
View File
@@ -30,7 +30,7 @@ type pather struct {
type coverer struct {
ctx driver.Device
pipelines [3]*pipeline
pipelines [2][3]*pipeline
texUniforms *coverTexUniforms
colUniforms *coverColUniforms
linearGradientUniforms *coverLinearGradientUniforms
@@ -309,7 +309,9 @@ func (p *pather) release() {
func (c *coverer) release() {
for _, p := range c.pipelines {
p.Release()
for _, p := range p {
p.Release()
}
}
}
@@ -405,7 +407,11 @@ func (c *coverer) cover(mat materialType, isFBO bool, col f32color.RGBA, col1, c
}
uniforms.transform = [4]float32{scale.X, scale.Y, off.X, off.Y}
uniforms.uvCoverTransform = [4]float32{coverScale.X, coverScale.Y, coverOff.X, coverOff.Y}
c.pipelines[mat].UploadUniforms(c.ctx)
fboIdx := 0
if isFBO {
fboIdx = 1
}
c.pipelines[fboIdx][mat].UploadUniforms(c.ctx)
c.ctx.DrawArrays(0, 4)
}
+4 -7
View File
@@ -15,10 +15,9 @@ import (
)
type Context struct {
disp _EGLDisplay
eglCtx *eglContext
eglSurf _EGLSurface
width, height int
disp _EGLDisplay
eglCtx *eglContext
eglSurf _EGLSurface
}
type eglContext struct {
@@ -121,11 +120,9 @@ func (c *Context) VisualID() int {
return c.eglCtx.visualID
}
func (c *Context) CreateSurface(win NativeWindowType, width, height int) error {
func (c *Context) CreateSurface(win NativeWindowType) error {
eglSurf, err := createSurface(c.disp, c.eglCtx, win)
c.eglSurf = eglSurf
c.width = width
c.height = height
return err
}
+48 -26
View File
@@ -9,8 +9,6 @@ import (
"unsafe"
syscall "golang.org/x/sys/windows"
"gioui.org/internal/gl"
)
type (
@@ -24,23 +22,23 @@ type (
)
var (
libEGL = syscall.NewLazyDLL("libEGL.dll")
_eglChooseConfig = libEGL.NewProc("eglChooseConfig")
_eglCreateContext = libEGL.NewProc("eglCreateContext")
_eglCreateWindowSurface = libEGL.NewProc("eglCreateWindowSurface")
_eglDestroyContext = libEGL.NewProc("eglDestroyContext")
_eglDestroySurface = libEGL.NewProc("eglDestroySurface")
_eglGetConfigAttrib = libEGL.NewProc("eglGetConfigAttrib")
_eglGetDisplay = libEGL.NewProc("eglGetDisplay")
_eglGetError = libEGL.NewProc("eglGetError")
_eglInitialize = libEGL.NewProc("eglInitialize")
_eglMakeCurrent = libEGL.NewProc("eglMakeCurrent")
_eglReleaseThread = libEGL.NewProc("eglReleaseThread")
_eglSwapInterval = libEGL.NewProc("eglSwapInterval")
_eglSwapBuffers = libEGL.NewProc("eglSwapBuffers")
_eglTerminate = libEGL.NewProc("eglTerminate")
_eglQueryString = libEGL.NewProc("eglQueryString")
_eglWaitClient = libEGL.NewProc("eglWaitClient")
libEGL = syscall.DLL{}
_eglChooseConfig *syscall.Proc
_eglCreateContext *syscall.Proc
_eglCreateWindowSurface *syscall.Proc
_eglDestroyContext *syscall.Proc
_eglDestroySurface *syscall.Proc
_eglGetConfigAttrib *syscall.Proc
_eglGetDisplay *syscall.Proc
_eglGetError *syscall.Proc
_eglInitialize *syscall.Proc
_eglMakeCurrent *syscall.Proc
_eglReleaseThread *syscall.Proc
_eglSwapInterval *syscall.Proc
_eglSwapBuffers *syscall.Proc
_eglTerminate *syscall.Proc
_eglQueryString *syscall.Proc
_eglWaitClient *syscall.Proc
)
var loadOnce sync.Once
@@ -54,21 +52,45 @@ func loadEGL() error {
}
func loadDLLs() error {
if err := loadDLL(libEGL, "libEGL.dll"); err != nil {
if err := loadDLL(&libEGL, "libEGL.dll"); err != nil {
return err
}
if err := loadDLL(gl.LibGLESv2, "libGLESv2.dll"); err != nil {
return err
procs := map[string]**syscall.Proc{
"eglChooseConfig": &_eglChooseConfig,
"eglCreateContext": &_eglCreateContext,
"eglCreateWindowSurface": &_eglCreateWindowSurface,
"eglDestroyContext": &_eglDestroyContext,
"eglDestroySurface": &_eglDestroySurface,
"eglGetConfigAttrib": &_eglGetConfigAttrib,
"eglGetDisplay": &_eglGetDisplay,
"eglGetError": &_eglGetError,
"eglInitialize": &_eglInitialize,
"eglMakeCurrent": &_eglMakeCurrent,
"eglReleaseThread": &_eglReleaseThread,
"eglSwapInterval": &_eglSwapInterval,
"eglSwapBuffers": &_eglSwapBuffers,
"eglTerminate": &_eglTerminate,
"eglQueryString": &_eglQueryString,
"eglWaitClient": &_eglWaitClient,
}
// d3dcompiler_47.dll is needed internally for shader compilation to function.
return loadDLL(syscall.NewLazyDLL("d3dcompiler_47.dll"), "d3dcompiler_47.dll")
for name, proc := range procs {
p, err := libEGL.FindProc(name)
if err != nil {
return fmt.Errorf("failed to locate %s in %s: %w", name, libEGL.Name, err)
}
*proc = p
}
return nil
}
func loadDLL(dll *syscall.LazyDLL, name string) error {
err := dll.Load()
func loadDLL(dll *syscall.DLL, name string) error {
handle, err := syscall.LoadLibraryEx(name, 0, syscall.LOAD_LIBRARY_SEARCH_DEFAULT_DIRS)
if err != nil {
return fmt.Errorf("egl: failed to load %s: %v", name, err)
}
dll.Handle = handle
dll.Name = name
return nil
}
+210 -89
View File
@@ -3,103 +3,217 @@
package gl
import (
"fmt"
"math"
"runtime"
"sync"
"syscall"
"unsafe"
"golang.org/x/sys/windows"
)
func loadGLESv2Procs() error {
dllName := "libGLESv2.dll"
handle, err := windows.LoadLibraryEx(dllName, 0, windows.LOAD_LIBRARY_SEARCH_DEFAULT_DIRS)
if err != nil {
return fmt.Errorf("gl: failed to load %s: %v", dllName, err)
}
gles := windows.DLL{Handle: handle, Name: dllName}
// d3dcompiler_47.dll is needed internally for shader compilation to function.
dllName = "d3dcompiler_47.dll"
_, err = windows.LoadLibraryEx(dllName, 0, windows.LOAD_LIBRARY_SEARCH_DEFAULT_DIRS)
if err != nil {
return fmt.Errorf("gl: failed to load %s: %v", dllName, err)
}
procs := map[string]**windows.Proc{
"glActiveTexture": &_glActiveTexture,
"glAttachShader": &_glAttachShader,
"glBeginQuery": &_glBeginQuery,
"glBindAttribLocation": &_glBindAttribLocation,
"glBindBuffer": &_glBindBuffer,
"glBindBufferBase": &_glBindBufferBase,
"glBindFramebuffer": &_glBindFramebuffer,
"glBindRenderbuffer": &_glBindRenderbuffer,
"glBindTexture": &_glBindTexture,
"glBindVertexArray": &_glBindVertexArray,
"glBlendEquation": &_glBlendEquation,
"glBlendFuncSeparate": &_glBlendFuncSeparate,
"glBufferData": &_glBufferData,
"glBufferSubData": &_glBufferSubData,
"glCheckFramebufferStatus": &_glCheckFramebufferStatus,
"glClear": &_glClear,
"glClearColor": &_glClearColor,
"glClearDepthf": &_glClearDepthf,
"glDeleteQueries": &_glDeleteQueries,
"glDeleteVertexArrays": &_glDeleteVertexArrays,
"glCompileShader": &_glCompileShader,
"glCopyTexSubImage2D": &_glCopyTexSubImage2D,
"glGenerateMipmap": &_glGenerateMipmap,
"glGenBuffers": &_glGenBuffers,
"glGenFramebuffers": &_glGenFramebuffers,
"glGenVertexArrays": &_glGenVertexArrays,
"glGetUniformBlockIndex": &_glGetUniformBlockIndex,
"glCreateProgram": &_glCreateProgram,
"glGenRenderbuffers": &_glGenRenderbuffers,
"glCreateShader": &_glCreateShader,
"glGenTextures": &_glGenTextures,
"glDeleteBuffers": &_glDeleteBuffers,
"glDeleteFramebuffers": &_glDeleteFramebuffers,
"glDeleteProgram": &_glDeleteProgram,
"glDeleteShader": &_glDeleteShader,
"glDeleteRenderbuffers": &_glDeleteRenderbuffers,
"glDeleteTextures": &_glDeleteTextures,
"glDepthFunc": &_glDepthFunc,
"glDepthMask": &_glDepthMask,
"glDisableVertexAttribArray": &_glDisableVertexAttribArray,
"glDisable": &_glDisable,
"glDrawArrays": &_glDrawArrays,
"glDrawElements": &_glDrawElements,
"glEnable": &_glEnable,
"glEnableVertexAttribArray": &_glEnableVertexAttribArray,
"glEndQuery": &_glEndQuery,
"glFinish": &_glFinish,
"glFlush": &_glFlush,
"glFramebufferRenderbuffer": &_glFramebufferRenderbuffer,
"glFramebufferTexture2D": &_glFramebufferTexture2D,
"glGenQueries": &_glGenQueries,
"glGetError": &_glGetError,
"glGetRenderbufferParameteriv": &_glGetRenderbufferParameteriv,
"glGetFloatv": &_glGetFloatv,
"glGetFramebufferAttachmentParameteriv": &_glGetFramebufferAttachmentParameteriv,
"glGetIntegerv": &_glGetIntegerv,
"glGetIntegeri_v": &_glGetIntegeri_v,
"glGetProgramiv": &_glGetProgramiv,
"glGetProgramInfoLog": &_glGetProgramInfoLog,
"glGetQueryObjectuiv": &_glGetQueryObjectuiv,
"glGetShaderiv": &_glGetShaderiv,
"glGetShaderInfoLog": &_glGetShaderInfoLog,
"glGetString": &_glGetString,
"glGetUniformLocation": &_glGetUniformLocation,
"glGetVertexAttribiv": &_glGetVertexAttribiv,
"glGetVertexAttribPointerv": &_glGetVertexAttribPointerv,
"glInvalidateFramebuffer": &_glInvalidateFramebuffer,
"glIsEnabled": &_glIsEnabled,
"glLinkProgram": &_glLinkProgram,
"glPixelStorei": &_glPixelStorei,
"glReadPixels": &_glReadPixels,
"glRenderbufferStorage": &_glRenderbufferStorage,
"glScissor": &_glScissor,
"glShaderSource": &_glShaderSource,
"glTexImage2D": &_glTexImage2D,
"glTexStorage2D": &_glTexStorage2D,
"glTexSubImage2D": &_glTexSubImage2D,
"glTexParameteri": &_glTexParameteri,
"glUniformBlockBinding": &_glUniformBlockBinding,
"glUniform1f": &_glUniform1f,
"glUniform1i": &_glUniform1i,
"glUniform2f": &_glUniform2f,
"glUniform3f": &_glUniform3f,
"glUniform4f": &_glUniform4f,
"glUseProgram": &_glUseProgram,
"glVertexAttribPointer": &_glVertexAttribPointer,
"glViewport": &_glViewport,
}
for name, proc := range procs {
p, err := gles.FindProc(name)
if err != nil {
return fmt.Errorf("failed to locate %s in %s: %w", name, gles.Name, err)
}
*proc = p
}
return nil
}
var (
LibGLESv2 = windows.NewLazyDLL("libGLESv2.dll")
_glActiveTexture = LibGLESv2.NewProc("glActiveTexture")
_glAttachShader = LibGLESv2.NewProc("glAttachShader")
_glBeginQuery = LibGLESv2.NewProc("glBeginQuery")
_glBindAttribLocation = LibGLESv2.NewProc("glBindAttribLocation")
_glBindBuffer = LibGLESv2.NewProc("glBindBuffer")
_glBindBufferBase = LibGLESv2.NewProc("glBindBufferBase")
_glBindFramebuffer = LibGLESv2.NewProc("glBindFramebuffer")
_glBindRenderbuffer = LibGLESv2.NewProc("glBindRenderbuffer")
_glBindTexture = LibGLESv2.NewProc("glBindTexture")
_glBindVertexArray = LibGLESv2.NewProc("glBindVertexArray")
_glBlendEquation = LibGLESv2.NewProc("glBlendEquation")
_glBlendFuncSeparate = LibGLESv2.NewProc("glBlendFuncSeparate")
_glBufferData = LibGLESv2.NewProc("glBufferData")
_glBufferSubData = LibGLESv2.NewProc("glBufferSubData")
_glCheckFramebufferStatus = LibGLESv2.NewProc("glCheckFramebufferStatus")
_glClear = LibGLESv2.NewProc("glClear")
_glClearColor = LibGLESv2.NewProc("glClearColor")
_glClearDepthf = LibGLESv2.NewProc("glClearDepthf")
_glDeleteQueries = LibGLESv2.NewProc("glDeleteQueries")
_glDeleteVertexArrays = LibGLESv2.NewProc("glDeleteVertexArrays")
_glCompileShader = LibGLESv2.NewProc("glCompileShader")
_glCopyTexSubImage2D = LibGLESv2.NewProc("glCopyTexSubImage2D")
_glGenerateMipmap = LibGLESv2.NewProc("glGenerateMipmap")
_glGenBuffers = LibGLESv2.NewProc("glGenBuffers")
_glGenFramebuffers = LibGLESv2.NewProc("glGenFramebuffers")
_glGenVertexArrays = LibGLESv2.NewProc("glGenVertexArrays")
_glGetUniformBlockIndex = LibGLESv2.NewProc("glGetUniformBlockIndex")
_glCreateProgram = LibGLESv2.NewProc("glCreateProgram")
_glGenRenderbuffers = LibGLESv2.NewProc("glGenRenderbuffers")
_glCreateShader = LibGLESv2.NewProc("glCreateShader")
_glGenTextures = LibGLESv2.NewProc("glGenTextures")
_glDeleteBuffers = LibGLESv2.NewProc("glDeleteBuffers")
_glDeleteFramebuffers = LibGLESv2.NewProc("glDeleteFramebuffers")
_glDeleteProgram = LibGLESv2.NewProc("glDeleteProgram")
_glDeleteShader = LibGLESv2.NewProc("glDeleteShader")
_glDeleteRenderbuffers = LibGLESv2.NewProc("glDeleteRenderbuffers")
_glDeleteTextures = LibGLESv2.NewProc("glDeleteTextures")
_glDepthFunc = LibGLESv2.NewProc("glDepthFunc")
_glDepthMask = LibGLESv2.NewProc("glDepthMask")
_glDisableVertexAttribArray = LibGLESv2.NewProc("glDisableVertexAttribArray")
_glDisable = LibGLESv2.NewProc("glDisable")
_glDrawArrays = LibGLESv2.NewProc("glDrawArrays")
_glDrawElements = LibGLESv2.NewProc("glDrawElements")
_glEnable = LibGLESv2.NewProc("glEnable")
_glEnableVertexAttribArray = LibGLESv2.NewProc("glEnableVertexAttribArray")
_glEndQuery = LibGLESv2.NewProc("glEndQuery")
_glFinish = LibGLESv2.NewProc("glFinish")
_glFlush = LibGLESv2.NewProc("glFlush")
_glFramebufferRenderbuffer = LibGLESv2.NewProc("glFramebufferRenderbuffer")
_glFramebufferTexture2D = LibGLESv2.NewProc("glFramebufferTexture2D")
_glGenQueries = LibGLESv2.NewProc("glGenQueries")
_glGetError = LibGLESv2.NewProc("glGetError")
_glGetRenderbufferParameteriv = LibGLESv2.NewProc("glGetRenderbufferParameteriv")
_glGetFloatv = LibGLESv2.NewProc("glGetFloatv")
_glGetFramebufferAttachmentParameteriv = LibGLESv2.NewProc("glGetFramebufferAttachmentParameteriv")
_glGetIntegerv = LibGLESv2.NewProc("glGetIntegerv")
_glGetIntegeri_v = LibGLESv2.NewProc("glGetIntegeri_v")
_glGetProgramiv = LibGLESv2.NewProc("glGetProgramiv")
_glGetProgramInfoLog = LibGLESv2.NewProc("glGetProgramInfoLog")
_glGetQueryObjectuiv = LibGLESv2.NewProc("glGetQueryObjectuiv")
_glGetShaderiv = LibGLESv2.NewProc("glGetShaderiv")
_glGetShaderInfoLog = LibGLESv2.NewProc("glGetShaderInfoLog")
_glGetString = LibGLESv2.NewProc("glGetString")
_glGetUniformLocation = LibGLESv2.NewProc("glGetUniformLocation")
_glGetVertexAttribiv = LibGLESv2.NewProc("glGetVertexAttribiv")
_glGetVertexAttribPointerv = LibGLESv2.NewProc("glGetVertexAttribPointerv")
_glInvalidateFramebuffer = LibGLESv2.NewProc("glInvalidateFramebuffer")
_glIsEnabled = LibGLESv2.NewProc("glIsEnabled")
_glLinkProgram = LibGLESv2.NewProc("glLinkProgram")
_glPixelStorei = LibGLESv2.NewProc("glPixelStorei")
_glReadPixels = LibGLESv2.NewProc("glReadPixels")
_glRenderbufferStorage = LibGLESv2.NewProc("glRenderbufferStorage")
_glScissor = LibGLESv2.NewProc("glScissor")
_glShaderSource = LibGLESv2.NewProc("glShaderSource")
_glTexImage2D = LibGLESv2.NewProc("glTexImage2D")
_glTexStorage2D = LibGLESv2.NewProc("glTexStorage2D")
_glTexSubImage2D = LibGLESv2.NewProc("glTexSubImage2D")
_glTexParameteri = LibGLESv2.NewProc("glTexParameteri")
_glUniformBlockBinding = LibGLESv2.NewProc("glUniformBlockBinding")
_glUniform1f = LibGLESv2.NewProc("glUniform1f")
_glUniform1i = LibGLESv2.NewProc("glUniform1i")
_glUniform2f = LibGLESv2.NewProc("glUniform2f")
_glUniform3f = LibGLESv2.NewProc("glUniform3f")
_glUniform4f = LibGLESv2.NewProc("glUniform4f")
_glUseProgram = LibGLESv2.NewProc("glUseProgram")
_glVertexAttribPointer = LibGLESv2.NewProc("glVertexAttribPointer")
_glViewport = LibGLESv2.NewProc("glViewport")
glInitOnce sync.Once
_glActiveTexture *windows.Proc
_glAttachShader *windows.Proc
_glBeginQuery *windows.Proc
_glBindAttribLocation *windows.Proc
_glBindBuffer *windows.Proc
_glBindBufferBase *windows.Proc
_glBindFramebuffer *windows.Proc
_glBindRenderbuffer *windows.Proc
_glBindTexture *windows.Proc
_glBindVertexArray *windows.Proc
_glBlendEquation *windows.Proc
_glBlendFuncSeparate *windows.Proc
_glBufferData *windows.Proc
_glBufferSubData *windows.Proc
_glCheckFramebufferStatus *windows.Proc
_glClear *windows.Proc
_glClearColor *windows.Proc
_glClearDepthf *windows.Proc
_glDeleteQueries *windows.Proc
_glDeleteVertexArrays *windows.Proc
_glCompileShader *windows.Proc
_glCopyTexSubImage2D *windows.Proc
_glGenerateMipmap *windows.Proc
_glGenBuffers *windows.Proc
_glGenFramebuffers *windows.Proc
_glGenVertexArrays *windows.Proc
_glGetUniformBlockIndex *windows.Proc
_glCreateProgram *windows.Proc
_glGenRenderbuffers *windows.Proc
_glCreateShader *windows.Proc
_glGenTextures *windows.Proc
_glDeleteBuffers *windows.Proc
_glDeleteFramebuffers *windows.Proc
_glDeleteProgram *windows.Proc
_glDeleteShader *windows.Proc
_glDeleteRenderbuffers *windows.Proc
_glDeleteTextures *windows.Proc
_glDepthFunc *windows.Proc
_glDepthMask *windows.Proc
_glDisableVertexAttribArray *windows.Proc
_glDisable *windows.Proc
_glDrawArrays *windows.Proc
_glDrawElements *windows.Proc
_glEnable *windows.Proc
_glEnableVertexAttribArray *windows.Proc
_glEndQuery *windows.Proc
_glFinish *windows.Proc
_glFlush *windows.Proc
_glFramebufferRenderbuffer *windows.Proc
_glFramebufferTexture2D *windows.Proc
_glGenQueries *windows.Proc
_glGetError *windows.Proc
_glGetRenderbufferParameteriv *windows.Proc
_glGetFloatv *windows.Proc
_glGetFramebufferAttachmentParameteriv *windows.Proc
_glGetIntegerv *windows.Proc
_glGetIntegeri_v *windows.Proc
_glGetProgramiv *windows.Proc
_glGetProgramInfoLog *windows.Proc
_glGetQueryObjectuiv *windows.Proc
_glGetShaderiv *windows.Proc
_glGetShaderInfoLog *windows.Proc
_glGetString *windows.Proc
_glGetUniformLocation *windows.Proc
_glGetVertexAttribiv *windows.Proc
_glGetVertexAttribPointerv *windows.Proc
_glInvalidateFramebuffer *windows.Proc
_glIsEnabled *windows.Proc
_glLinkProgram *windows.Proc
_glPixelStorei *windows.Proc
_glReadPixels *windows.Proc
_glRenderbufferStorage *windows.Proc
_glScissor *windows.Proc
_glShaderSource *windows.Proc
_glTexImage2D *windows.Proc
_glTexStorage2D *windows.Proc
_glTexSubImage2D *windows.Proc
_glTexParameteri *windows.Proc
_glUniformBlockBinding *windows.Proc
_glUniform1f *windows.Proc
_glUniform1i *windows.Proc
_glUniform2f *windows.Proc
_glUniform3f *windows.Proc
_glUniform4f *windows.Proc
_glUseProgram *windows.Proc
_glVertexAttribPointer *windows.Proc
_glViewport *windows.Proc
)
type Functions struct {
@@ -115,7 +229,11 @@ func NewFunctions(ctx Context, forceES bool) (*Functions, error) {
if ctx != nil {
panic("non-nil context")
}
return new(Functions), nil
var err error
glInitOnce.Do(func() {
err = loadGLESv2Procs()
})
return new(Functions), err
}
func (c *Functions) ActiveTexture(t Enum) {
@@ -361,6 +479,9 @@ func (c *Functions) GetProgrami(p Program, pname Enum) int {
}
func (c *Functions) GetProgramInfoLog(p Program) string {
n := c.GetProgrami(p, INFO_LOG_LENGTH)
if n == 0 {
return ""
}
buf := make([]byte, n)
syscall.Syscall6(_glGetProgramInfoLog.Addr(), 4, uintptr(p.V), uintptr(len(buf)), 0, uintptr(unsafe.Pointer(&buf[0])), 0, 0)
return string(buf)
+3
View File
@@ -327,6 +327,9 @@ func strokePathNorm(p0, p1, p2 f32.Point, t, d float32) f32.Point {
func rot90CW(p f32.Point) f32.Point { return f32.Pt(+p.Y, -p.X) }
func normPt(p f32.Point, l float32) f32.Point {
if (p.X == 0 && p.Y == l) || (p.Y == 0 && p.X == l) {
return f32.Point{X: p.X, Y: p.Y}
}
d := math.Hypot(float64(p.X), float64(p.Y))
l64 := float64(l)
if math.Abs(d-l64) < 1e-10 {
+6 -6
View File
@@ -1893,27 +1893,27 @@ func BuildWriteDescriptorSetBuffer(set DescriptorSet, binding int, typ Descripto
}
}
func (r PushConstantRange) StageFlags() ShaderStageFlags {
func PushConstantRangeStageFlags(r PushConstantRange) ShaderStageFlags {
return r.stageFlags
}
func (r PushConstantRange) Offset() int {
func PushConstantRangeOffset(r PushConstantRange) int {
return int(r.offset)
}
func (r PushConstantRange) Size() int {
func PushConstantRangeSize(r PushConstantRange) int {
return int(r.size)
}
func (p QueueFamilyProperties) Flags() QueueFlags {
func QueueFamilyPropertiesFlags(p QueueFamilyProperties) QueueFlags {
return p.queueFlags
}
func (c SurfaceCapabilities) MinExtent() image.Point {
func SurfaceCapabilitiesMinExtent(c SurfaceCapabilities) image.Point {
return image.Pt(int(c.minImageExtent.width), int(c.minImageExtent.height))
}
func (c SurfaceCapabilities) MaxExtent() image.Point {
func SurfaceCapabilitiesMaxExtent(c SurfaceCapabilities) image.Point {
return image.Pt(int(c.maxImageExtent.width), int(c.maxImageExtent.height))
}
+4 -3
View File
@@ -251,9 +251,10 @@ func TestFocusScroll(t *testing.T) {
filters := []event.Filter{
key.FocusFilter{Target: h},
pointer.Filter{
Target: h,
Kinds: pointer.Scroll,
ScrollBounds: image.Rect(-100, -100, 100, 100),
Target: h,
Kinds: pointer.Scroll,
ScrollX: pointer.ScrollRange{Min: -100, Max: +100},
ScrollY: pointer.ScrollRange{Min: -100, Max: +100},
},
}
events(r, -1, filters...)
+7 -5
View File
@@ -72,7 +72,7 @@ type pointerHandler struct {
type pointerFilter struct {
kinds pointer.Kind
// min and max horizontal/vertical scroll
scrollRange image.Rectangle
scrollX, scrollY pointer.ScrollRange
sourceMimes []string
targetMimes []string
@@ -297,7 +297,8 @@ func (p *pointerFilter) Add(f event.Filter) {
p.targetMimes = append(p.targetMimes, f.Type)
case pointer.Filter:
p.kinds = p.kinds | f.Kinds
p.scrollRange = p.scrollRange.Union(f.ScrollBounds)
p.scrollX = p.scrollX.Union(f.ScrollX)
p.scrollY = p.scrollY.Union(f.ScrollY)
}
}
@@ -325,7 +326,8 @@ func (p *pointerFilter) Matches(e event.Event) bool {
func (p *pointerFilter) Merge(p2 pointerFilter) {
p.kinds = p.kinds | p2.kinds
p.scrollRange = p.scrollRange.Union(p2.scrollRange)
p.scrollX = p.scrollX.Union(p2.scrollX)
p.scrollY = p.scrollY.Union(p2.scrollY)
p.sourceMimes = append(p.sourceMimes, p2.sourceMimes...)
p.targetMimes = append(p.targetMimes, p2.targetMimes...)
}
@@ -333,8 +335,8 @@ func (p *pointerFilter) Merge(p2 pointerFilter) {
// clampScroll splits a scroll distance in the remaining scroll and the
// scroll accepted by the filter.
func (p *pointerFilter) clampScroll(scroll f32.Point) (left, scrolled f32.Point) {
left.X, scrolled.X = clampSplit(scroll.X, p.scrollRange.Min.X, p.scrollRange.Max.X)
left.Y, scrolled.Y = clampSplit(scroll.Y, p.scrollRange.Min.Y, p.scrollRange.Max.Y)
left.X, scrolled.X = clampSplit(scroll.X, p.scrollX.Min, p.scrollX.Max)
left.Y, scrolled.Y = clampSplit(scroll.Y, p.scrollY.Min, p.scrollY.Max)
return
}
+37 -9
View File
@@ -300,9 +300,9 @@ func TestPointerPriority(t *testing.T) {
r1 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops)
f1 := func(t event.Tag) event.Filter {
return pointer.Filter{
Target: t,
Kinds: pointer.Scroll,
ScrollBounds: image.Rectangle{Max: image.Point{X: 100}},
Target: t,
Kinds: pointer.Scroll,
ScrollX: pointer.ScrollRange{Max: 100},
}
}
events(&r, -1, f1(handler1))
@@ -311,9 +311,9 @@ func TestPointerPriority(t *testing.T) {
r2 := clip.Rect(image.Rect(0, 0, 100, 50)).Push(&ops)
f2 := func(t event.Tag) event.Filter {
return pointer.Filter{
Target: t,
Kinds: pointer.Scroll,
ScrollBounds: image.Rectangle{Max: image.Point{X: 20}},
Target: t,
Kinds: pointer.Scroll,
ScrollX: pointer.ScrollRange{Max: 20},
}
}
events(&r, -1, f2(handler2))
@@ -324,9 +324,10 @@ func TestPointerPriority(t *testing.T) {
r3 := clip.Rect(image.Rect(0, 100, 100, 200)).Push(&ops)
f3 := func(t event.Tag) event.Filter {
return pointer.Filter{
Target: t,
Kinds: pointer.Scroll,
ScrollBounds: image.Rectangle{Min: image.Point{X: -20, Y: -40}},
Target: t,
Kinds: pointer.Scroll,
ScrollX: pointer.ScrollRange{Min: -20},
ScrollY: pointer.ScrollRange{Min: -40},
}
}
events(&r, -1, f3(handler3))
@@ -1085,6 +1086,33 @@ func TestPassCursor(t *testing.T) {
}
}
func TestPartialEvent(t *testing.T) {
var ops op.Ops
var r Router
rect := clip.Rect(image.Rect(0, 0, 100, 100))
background := rect.Push(&ops)
event.Op(&ops, 1)
background.Pop()
overlayPass := pointer.PassOp{}.Push(&ops)
overlay := rect.Push(&ops)
event.Op(&ops, 2)
overlay.Pop()
overlayPass.Pop()
assertEventSequence(t, events(&r, -1, pointer.Filter{Target: 1, Kinds: pointer.Press}))
assertEventSequence(t, events(&r, -1, pointer.Filter{Target: 2, Kinds: pointer.Press}))
r.Frame(&ops)
r.Queue(pointer.Event{
Kind: pointer.Press,
})
assertEventSequence(t, events(&r, -1, pointer.Filter{Target: 1, Kinds: pointer.Press}, key.FocusFilter{Target: 1}),
key.FocusEvent{}, pointer.Event{Kind: pointer.Press, Source: pointer.Mouse, Priority: pointer.Shared})
r.Source().Execute(key.FocusCmd{Tag: 1})
assertEventSequence(t, events(&r, -1, pointer.Filter{Target: 2, Kinds: pointer.Press}),
pointer.Event{Kind: pointer.Press, Source: pointer.Mouse, Priority: pointer.Foremost})
}
// offer satisfies io.ReadCloser for use in data transfers.
type offer struct {
data string
+35 -36
View File
@@ -35,10 +35,9 @@ type Router struct {
queue keyQueue
// The following fields have the same purpose as the fields in
// type handler, but for key.Events.
filter keyFilter
nextFilter keyFilter
processedFilter keyFilter
scratchFilter keyFilter
filter keyFilter
nextFilter keyFilter
scratchFilter keyFilter
}
cqueue clipboardQueue
// states is the list of pending state changes resulting from
@@ -61,7 +60,7 @@ type Router struct {
}
// Source implements the interface between a Router and user interface widgets.
// The value Source is disabled.
// The zero-value Source is disabled.
type Source struct {
r *Router
}
@@ -172,22 +171,22 @@ func (q *Router) Source() Source {
// Execute a command.
func (s Source) Execute(c Command) {
if !s.Enabled() {
if !s.enabled() {
return
}
s.r.execute(c)
}
// Enabled reports whether the source is enabled. Only enabled
// enabled reports whether the source is enabled. Only enabled
// Sources deliver events and respond to commands.
func (s Source) Enabled() bool {
func (s Source) enabled() bool {
return s.r != nil
}
// Focused reports whether tag is focused, according to the most recent
// [key.FocusEvent] delivered.
func (s Source) Focused(tag event.Tag) bool {
if !s.Enabled() {
if !s.enabled() {
return false
}
return s.r.state().keyState.focus == tag
@@ -195,7 +194,7 @@ func (s Source) Focused(tag event.Tag) bool {
// Event returns the next event that matches at least one of filters.
func (s Source) Event(filters ...event.Filter) (event.Event, bool) {
if !s.Enabled() {
if !s.enabled() {
return nil, false
}
return s.r.Event(filters...)
@@ -275,28 +274,29 @@ func (q *Router) Event(filters ...event.Filter) (event.Event, bool) {
}
}
}
if !q.deferring {
for i := range q.changes {
change := &q.changes[i]
for j, evt := range change.events {
match := false
switch e := evt.event.(type) {
case key.Event:
match = q.key.scratchFilter.Matches(change.state.keyState.focus, e, false)
default:
for _, tf := range q.scratchFilters {
if evt.tag == tf.tag && tf.filter.Matches(evt.event) {
match = true
break
}
for i := range q.changes {
if q.deferring && i > 0 {
break
}
change := &q.changes[i]
for j, evt := range change.events {
match := false
switch e := evt.event.(type) {
case key.Event:
match = q.key.scratchFilter.Matches(change.state.keyState.focus, e, false)
default:
for _, tf := range q.scratchFilters {
if evt.tag == tf.tag && tf.filter.Matches(evt.event) {
match = true
break
}
}
if match {
change.events = append(change.events[:j], change.events[j+1:]...)
// Fast forward state to last matched.
q.collapseState(i)
return evt.event, true
}
}
if match {
change.events = append(change.events[:j], change.events[j+1:]...)
// Fast forward state to last matched.
q.collapseState(i)
return evt.event, true
}
}
}
@@ -304,7 +304,6 @@ func (q *Router) Event(filters ...event.Filter) (event.Event, bool) {
h := q.stateFor(tf.tag)
h.processedFilter.Merge(tf.filter)
}
q.key.processedFilter = append(q.key.processedFilter, q.key.scratchFilter...)
return nil, false
}
@@ -315,15 +314,15 @@ func (q *Router) collapseState(idx int) {
}
first := &q.changes[0]
first.state = q.changes[idx].state
for i := 1; i <= idx; i++ {
first.events = append(first.events, q.changes[i].events...)
for _, ch := range q.changes[1 : idx+1] {
first.events = append(first.events, ch.events...)
}
q.changes = append(q.changes[:1], q.changes[idx+1:]...)
}
// Frame replaces the declared handlers from the supplied
// operation list. The text input state, wakeup time and whether
// there are active profile handlers is also saved.
// Frame completes the current frame and starts a new with the
// handlers from the frame argument. Remaining events are discarded,
// unless they were deferred by a command.
func (q *Router) Frame(frame *op.Ops) {
var remaining []event.Event
if n := len(q.changes); n > 0 {
+2 -2
View File
@@ -37,11 +37,11 @@ For example:
var h1, h2 *Handler
area := clip.Rect(...).Push(ops)
event.Op{Tag: h1}.Add(Ops)
event.Op(Ops, h1)
area.Pop()
area := clip.Rect(...).Push(ops)
event.Op{Tag: h2}.Add(ops)
event.Op(Ops, h2)
area.Pop()
implies a tree of two inner nodes, each with one pointer handler attached.
+19 -6
View File
@@ -3,7 +3,6 @@
package pointer
import (
"image"
"strings"
"time"
@@ -61,12 +60,19 @@ type Filter struct {
Target event.Tag
// Kinds is a bitwise-or of event types to match.
Kinds Kind
// ScrollBounds describe the maximum scrollable distances in both
// axes. Specifically, any Event e delivered to Tag will satisfy
// ScrollX and ScrollY constrain the range of scrolling events delivered
// to Target. Specifically, any Event e delivered to Tag will satisfy
//
// ScrollBounds.Min.X <= e.Scroll.X <= ScrollBounds.Max.X (horizontal axis)
// ScrollBounds.Min.Y <= e.Scroll.Y <= ScrollBounds.Max.Y (vertical axis)
ScrollBounds image.Rectangle
// ScrollX.Min <= e.Scroll.X <= ScrollX.Max (horizontal axis)
// ScrollY.Min <= e.Scroll.Y <= ScrollY.Max (vertical axis)
ScrollX ScrollRange
ScrollY ScrollRange
}
// ScrollRange describes the range of scrolling distances in an
// axis.
type ScrollRange struct {
Min, Max int
}
// GrabCmd requests a pointer grab on the pointer identified by ID.
@@ -219,6 +225,13 @@ const (
ButtonTertiary
)
func (s ScrollRange) Union(s2 ScrollRange) ScrollRange {
return ScrollRange{
Min: min(s.Min, s2.Min),
Max: max(s.Max, s2.Max),
}
}
// Push the current pass mode to the pass stack and set the pass mode.
func (p PassOp) Push(o *op.Ops) PassStack {
id, mid := ops.PushOp(&o.Internal, ops.PassStack)
+17 -3
View File
@@ -5,6 +5,7 @@ package layout
import (
"time"
"gioui.org/io/event"
"gioui.org/io/input"
"gioui.org/io/system"
"gioui.org/op"
@@ -28,6 +29,7 @@ type Context struct {
// Interested users must look up and populate these values manually.
Locale system.Locale
disabled bool
input.Source
*op.Ops
}
@@ -42,9 +44,21 @@ func (c Context) Sp(v unit.Sp) int {
return c.Metric.Sp(v)
}
// Disabled returns a copy of this context with a disabled Source,
// blocking widgets from changing its state and receiving events.
func (c Context) Event(filters ...event.Filter) (event.Event, bool) {
if c.disabled {
return nil, false
}
return c.Source.Event(filters...)
}
// Enabled reports whether this context is enabled. Disabled contexts
// don't report events.
func (c Context) Enabled() bool {
return !c.disabled
}
// Disabled returns a copy of this context that don't deliver any events.
func (c Context) Disabled() Context {
c.Source = input.Source{}
c.disabled = true
return c
}
+6 -4
View File
@@ -7,6 +7,7 @@ import (
"math"
"gioui.org/gesture"
"gioui.org/io/pointer"
"gioui.org/op"
"gioui.org/op/clip"
)
@@ -158,11 +159,12 @@ func (l *List) update(gtx Context) {
max = 0
}
}
scrollRange := image.Rectangle{
Min: l.Axis.Convert(image.Pt(min, 0)),
Max: l.Axis.Convert(image.Pt(max, 0)),
xrange := pointer.ScrollRange{Min: min, Max: max}
yrange := pointer.ScrollRange{}
if l.Axis == Vertical {
xrange, yrange = yrange, xrange
}
d := l.scroll.Update(gtx.Metric, gtx.Source, gtx.Now, gesture.Axis(l.Axis), scrollRange)
d := l.scroll.Update(gtx.Metric, gtx.Source, gtx.Now, gesture.Axis(l.Axis), xrange, yrange)
l.scrollDelta = d
l.Position.Offset += d
}
+3
View File
@@ -138,6 +138,9 @@ type Path struct {
func (p *Path) Pos() f32.Point { return p.pen }
// Begin the path, storing the path data and final Op into ops.
//
// Caller must also call End to finish the drawing.
// Forgetting to call it will result in a "panic: cannot mix multi ops with single ones".
func (p *Path) Begin(o *op.Ops) {
*p = Path{
ops: &o.Internal,
+39 -88
View File
@@ -12,10 +12,9 @@ import (
"github.com/go-text/typesetting/di"
"github.com/go-text/typesetting/font"
gotextot "github.com/go-text/typesetting/font/opentype"
"github.com/go-text/typesetting/fontscan"
"github.com/go-text/typesetting/language"
"github.com/go-text/typesetting/opentype/api"
"github.com/go-text/typesetting/opentype/api/metadata"
"github.com/go-text/typesetting/shaping"
"golang.org/x/exp/slices"
"golang.org/x/image/math/fixed"
@@ -201,7 +200,7 @@ type runLayout struct {
// Direction is the layout direction of the glyphs.
Direction system.TextDirection
// face is the font face that the ID of each Glyph in the Layout refers to.
face font.Face
face *font.Face
// truncator indicates that this run is a text truncator standing in for remaining
// text.
truncator bool
@@ -211,8 +210,8 @@ type runLayout struct {
type shaperImpl struct {
// Fields for tracking fonts/faces.
fontMap *fontscan.FontMap
faces []font.Face
faceToIndex map[font.Font]int
faces []*font.Face
faceToIndex map[*font.Font]int
faceMeta []giofont.Font
defaultFaces []string
logger interface {
@@ -254,7 +253,7 @@ func newShaperImpl(systemFonts bool, collection []FontFace) *shaperImpl {
var shaper shaperImpl
shaper.logger = newDebugLogger()
shaper.fontMap = fontscan.NewFontMap(shaper.logger)
shaper.faceToIndex = make(map[font.Font]int)
shaper.faceToIndex = make(map[*font.Font]int)
if systemFonts {
str, err := os.UserCacheDir()
if err != nil {
@@ -282,7 +281,7 @@ func (s *shaperImpl) Load(f FontFace) {
s.addFace(f.Face.Face(), f.Font)
}
func (s *shaperImpl) addFace(f font.Face, md giofont.Font) {
func (s *shaperImpl) addFace(f *font.Face, md giofont.Font) {
if _, ok := s.faceToIndex[f.Font]; ok {
return
}
@@ -377,11 +376,11 @@ func (s *shaperImpl) splitBidi(input shaping.Input) []shaping.Input {
// ResolveFace allows shaperImpl to implement shaping.FontMap, wrapping its fontMap
// field and ensuring that any faces loaded as part of the search are registered with
// ids so that they can be referred to by a GlyphID.
func (s *shaperImpl) ResolveFace(r rune) font.Face {
func (s *shaperImpl) ResolveFace(r rune) *font.Face {
face := s.fontMap.ResolveFace(r)
if face != nil {
family, aspect := s.fontMap.FontMetadata(face.Font)
md := opentype.DescriptionToFont(metadata.Description{
md := opentype.DescriptionToFont(font.Description{
Family: family,
Aspect: aspect,
})
@@ -491,9 +490,11 @@ func wrapPolicyToGoText(p WrapPolicy) shaping.LineBreakPolicy {
// shapeAndWrapText invokes the text shaper and returns wrapped lines in the shaper's native format.
func (s *shaperImpl) shapeAndWrapText(params Parameters, txt []rune) (_ []shaping.Line, truncated int) {
wc := shaping.WrapConfig{
TruncateAfterLines: params.MaxLines,
TextContinues: params.forceTruncate,
BreakPolicy: wrapPolicyToGoText(params.WrapPolicy),
Direction: mapDirection(params.Locale.Direction),
TruncateAfterLines: params.MaxLines,
TextContinues: params.forceTruncate,
BreakPolicy: wrapPolicyToGoText(params.WrapPolicy),
DisableTrailingWhitespaceTrim: params.DisableSpaceTrim,
}
families := s.defaultFaces
if params.Font.Typeface != "" {
@@ -663,7 +664,7 @@ func (s *shaperImpl) Shape(pathOps *op.Ops, gs []Glyph) clip.PathSpec {
scaleFactor := fixedToFloat(ppem) / float32(face.Upem())
glyphData := face.GlyphData(gid)
switch glyphData := glyphData.(type) {
case api.GlyphOutline:
case font.GlyphOutline:
outline := glyphData
// Move to glyph position.
pos := f32.Point{
@@ -678,9 +679,9 @@ func (s *shaperImpl) Shape(pathOps *op.Ops, gs []Glyph) clip.PathSpec {
for _, fseg := range outline.Segments {
nargs := 1
switch fseg.Op {
case api.SegmentOpQuadTo:
case gotextot.SegmentOpQuadTo:
nargs = 2
case api.SegmentOpCubeTo:
case gotextot.SegmentOpCubeTo:
nargs = 3
}
var args [3]f32.Point
@@ -695,13 +696,13 @@ func (s *shaperImpl) Shape(pathOps *op.Ops, gs []Glyph) clip.PathSpec {
}
}
switch fseg.Op {
case api.SegmentOpMoveTo:
case gotextot.SegmentOpMoveTo:
builder.Move(args[0])
case api.SegmentOpLineTo:
case gotextot.SegmentOpLineTo:
builder.Line(args[0])
case api.SegmentOpQuadTo:
case gotextot.SegmentOpQuadTo:
builder.Quad(args[0], args[1])
case api.SegmentOpCubeTo:
case gotextot.SegmentOpCubeTo:
builder.Cube(args[0], args[1], args[2])
default:
panic("unsupported segment op")
@@ -742,16 +743,16 @@ func (s *shaperImpl) Bitmaps(ops *op.Ops, gs []Glyph) op.CallOp {
}
glyphData := face.GlyphData(gid)
switch glyphData := glyphData.(type) {
case api.GlyphBitmap:
case font.GlyphBitmap:
var imgOp paint.ImageOp
var imgSize image.Point
bitmapData, ok := s.bitmapGlyphCache.Get(g.ID)
if !ok {
var img image.Image
switch glyphData.Format {
case api.PNG, api.JPG, api.TIFF:
case font.PNG, font.JPG, font.TIFF:
img, _, _ = image.Decode(bytes.NewReader(glyphData.Data))
case api.BlackAndWhite:
case font.BlackAndWhite:
// This is a complex family of uncompressed bitmaps that don't seem to be
// very common in practice. We can try adding support later if needed.
fallthrough
@@ -807,7 +808,7 @@ type langConfig struct {
}
// toInput converts its parameters into a shaping.Input.
func toInput(face font.Face, ppem fixed.Int26_6, lc langConfig, runes []rune) shaping.Input {
func toInput(face *font.Face, ppem fixed.Int26_6, lc langConfig, runes []rune) shaping.Input {
var input shaping.Input
input.Direction = lc.Direction
input.Text = runes
@@ -867,13 +868,14 @@ func toGioGlyphs(in []shaping.Glyph, ppem fixed.Int26_6, faceIdx int) []glyph {
}
// toLine converts the output into a Line with the provided dominant text direction.
func toLine(faceToIndex map[font.Font]int, o shaping.Line, dir system.TextDirection) line {
func toLine(faceToIndex map[*font.Font]int, o shaping.Line, dir system.TextDirection) line {
if len(o) < 1 {
return line{}
}
line := line{
runs: make([]runLayout, len(o)),
direction: dir,
runs: make([]runLayout, len(o)),
direction: dir,
visualOrder: make([]int, len(o)),
}
maxSize := fixed.Int26_6(0)
for i := range o {
@@ -881,7 +883,7 @@ func toLine(faceToIndex map[font.Font]int, o shaping.Line, dir system.TextDirect
if run.Size > maxSize {
maxSize = run.Size
}
var font font.Font
var font *font.Font
if run.Face != nil {
font = run.Face.Font
}
@@ -891,11 +893,13 @@ func toLine(faceToIndex map[font.Font]int, o shaping.Line, dir system.TextDirect
Count: run.Runes.Count,
Offset: line.runeCount,
},
Direction: unmapDirection(run.Direction),
face: run.Face,
Advance: run.Advance,
PPEM: run.Size,
Direction: unmapDirection(run.Direction),
face: run.Face,
Advance: run.Advance,
PPEM: run.Size,
VisualPosition: int(run.VisualIndex),
}
line.visualOrder[run.VisualIndex] = i
line.runeCount += run.Runes.Count
line.width += run.Advance
if line.ascent < run.LineBounds.Ascent {
@@ -906,64 +910,11 @@ func toLine(faceToIndex map[font.Font]int, o shaping.Line, dir system.TextDirect
}
}
line.lineHeight = maxSize
computeVisualOrder(&line)
return line
}
// computeVisualOrder will populate the Line's VisualOrder field and the
// VisualPosition field of each element in Runs.
func computeVisualOrder(l *line) {
l.visualOrder = make([]int, len(l.runs))
const none = -1
bidiRangeStart := none
// visPos returns the visual position for an individual logically-indexed
// run in this line, taking only the line's overall text direction into
// account.
visPos := func(logicalIndex int) int {
if l.direction.Progression() == system.TowardOrigin {
return len(l.runs) - 1 - logicalIndex
}
return logicalIndex
}
// resolveBidi populated the line's VisualOrder fields for the elements in the
// half-open range [bidiRangeStart:bidiRangeEnd) indicating that those elements
// should be displayed in reverse-visual order.
resolveBidi := func(bidiRangeStart, bidiRangeEnd int) {
firstVisual := bidiRangeEnd - 1
// Just found the end of a bidi range.
for startIdx := bidiRangeStart; startIdx < bidiRangeEnd; startIdx++ {
pos := visPos(firstVisual)
l.runs[startIdx].VisualPosition = pos
l.visualOrder[pos] = startIdx
firstVisual--
}
bidiRangeStart = none
}
for runIdx, run := range l.runs {
if run.Direction.Progression() != l.direction.Progression() {
if bidiRangeStart == none {
bidiRangeStart = runIdx
}
continue
} else if bidiRangeStart != none {
// Just found the end of a bidi range.
resolveBidi(bidiRangeStart, runIdx)
bidiRangeStart = none
}
pos := visPos(runIdx)
l.runs[runIdx].VisualPosition = pos
l.visualOrder[pos] = runIdx
}
if bidiRangeStart != none {
// We ended iteration within a bidi segment, resolve it.
resolveBidi(bidiRangeStart, len(l.runs))
}
// Iterate and resolve the X of each run.
x := fixed.Int26_6(0)
for _, runIdx := range l.visualOrder {
l.runs[runIdx].X = x
x += l.runs[runIdx].Advance
for _, runIdx := range line.visualOrder {
line.runs[runIdx].X = x
x += line.runs[runIdx].Advance
}
return line
}
-115
View File
@@ -3,7 +3,6 @@ package text
import (
"fmt"
"math"
"reflect"
"strconv"
"testing"
@@ -450,120 +449,6 @@ func TestToLine(t *testing.T) {
}
}
func TestComputeVisualOrder(t *testing.T) {
type testcase struct {
name string
input line
expectedVisualOrder []int
}
for _, tc := range []testcase{
{
name: "ltr",
input: line{
direction: system.LTR,
runs: []runLayout{
{Direction: system.LTR},
{Direction: system.LTR},
{Direction: system.LTR},
},
},
expectedVisualOrder: []int{0, 1, 2},
},
{
name: "rtl",
input: line{
direction: system.RTL,
runs: []runLayout{
{Direction: system.RTL},
{Direction: system.RTL},
{Direction: system.RTL},
},
},
expectedVisualOrder: []int{2, 1, 0},
},
{
name: "bidi-ltr",
input: line{
direction: system.LTR,
runs: []runLayout{
{Direction: system.LTR},
{Direction: system.RTL},
{Direction: system.RTL},
{Direction: system.RTL},
{Direction: system.LTR},
},
},
expectedVisualOrder: []int{0, 3, 2, 1, 4},
},
{
name: "bidi-ltr-complex",
input: line{
direction: system.LTR,
runs: []runLayout{
{Direction: system.RTL},
{Direction: system.RTL},
{Direction: system.LTR},
{Direction: system.RTL},
{Direction: system.RTL},
{Direction: system.LTR},
{Direction: system.RTL},
{Direction: system.RTL},
{Direction: system.LTR},
{Direction: system.RTL},
{Direction: system.RTL},
},
},
expectedVisualOrder: []int{1, 0, 2, 4, 3, 5, 7, 6, 8, 10, 9},
},
{
name: "bidi-rtl",
input: line{
direction: system.RTL,
runs: []runLayout{
{Direction: system.RTL},
{Direction: system.LTR},
{Direction: system.LTR},
{Direction: system.LTR},
{Direction: system.RTL},
},
},
expectedVisualOrder: []int{4, 1, 2, 3, 0},
},
{
name: "bidi-rtl-complex",
input: line{
direction: system.RTL,
runs: []runLayout{
{Direction: system.LTR},
{Direction: system.LTR},
{Direction: system.RTL},
{Direction: system.LTR},
{Direction: system.LTR},
{Direction: system.RTL},
{Direction: system.LTR},
{Direction: system.LTR},
{Direction: system.RTL},
{Direction: system.LTR},
{Direction: system.LTR},
},
},
expectedVisualOrder: []int{9, 10, 8, 6, 7, 5, 3, 4, 2, 0, 1},
},
} {
t.Run(tc.name, func(t *testing.T) {
computeVisualOrder(&tc.input)
if !reflect.DeepEqual(tc.input.visualOrder, tc.expectedVisualOrder) {
t.Errorf("expected visual order %v, got %v", tc.expectedVisualOrder, tc.input.visualOrder)
}
for i, visualIndex := range tc.input.visualOrder {
if pos := tc.input.runs[visualIndex].VisualPosition; pos != i {
t.Errorf("line.VisualOrder[%d]=%d, but line.Runs[%d].VisualPosition=%d", i, visualIndex, visualIndex, pos)
}
}
})
}
}
func FuzzLayout(f *testing.F) {
ltrFace, _ := opentype.Parse(goregular.TTF)
rtlFace, _ := opentype.Parse(nsareg.TTF)
+14 -1
View File
@@ -76,6 +76,11 @@ type Parameters struct {
// text with a MaxLines. It is unexported because this behavior only makes sense for the
// shaper to control when it iterates paragraphs of text.
forceTruncate bool
// DisableSpaceTrim prevents the width of the final whitespace glyph on a line from being zeroed.
// This is desirable for text editors (so that the whitespace can be selected), but is undesirable
// for ordinary display text.
DisableSpaceTrim bool
}
type FontFace = giofont.FontFace
@@ -199,7 +204,15 @@ func (f Flags) String() string {
type GlyphID uint64
// Shaper converts strings of text into glyphs that can be displayed.
// Shaper converts strings of text into glyphs that can be displayed. The same
// Shaper should not be used in different goroutines.
//
// The Shaper controls text layout and has a cache, implemented as a map, and
// so laying out text in two different goroutines can easily result in
// concurrent access to said map, resulting in a panic.
//
// Practically speaking, this means you should use different Shapers for
// different top-level windows.
type Shaper struct {
config struct {
disableSystemFonts bool
+6 -23
View File
@@ -11,8 +11,11 @@ import (
// Decorations handles the states of window decorations.
type Decorations struct {
// Maximized controls the look and behaviour of the maximize
// button. It is the user's responsibility to set Maximized
// according to the window state reported through [app.ConfigEvent].
Maximized bool
clicks map[int]*Clickable
maximized bool
}
// LayoutMove lays out the widget that makes a window movable.
@@ -40,17 +43,6 @@ func (d *Decorations) Clickable(action system.Action) *Clickable {
return click
}
// Perform updates the decorations as if the specified actions were
// performed by the user.
func (d *Decorations) Perform(actions system.Action) {
if actions&system.ActionMaximize != 0 {
d.maximized = true
}
if actions&(system.ActionUnmaximize|system.ActionMinimize|system.ActionFullscreen) != 0 {
d.maximized = false
}
}
// Update the state and return the set of actions activated by the user.
func (d *Decorations) Update(gtx layout.Context) system.Action {
var actions system.Action
@@ -60,21 +52,12 @@ func (d *Decorations) Update(gtx layout.Context) system.Action {
}
action := system.Action(1 << idx)
switch {
case action == system.ActionMaximize && d.maximized:
case action == system.ActionMaximize && d.Maximized:
action = system.ActionUnmaximize
case action == system.ActionUnmaximize && !d.maximized:
case action == system.ActionUnmaximize && !d.Maximized:
action = system.ActionMaximize
}
switch action {
case system.ActionMaximize, system.ActionUnmaximize:
d.maximized = !d.maximized
}
actions |= action
}
return actions
}
// Maximized returns whether the window is maximized.
func (d *Decorations) Maximized() bool {
return d.maximized
}
+21 -13
View File
@@ -228,19 +228,19 @@ func (e *Editor) processPointer(gtx layout.Context) (EditorEvent, bool) {
axis = gesture.Vertical
smin, smax = sbounds.Min.Y, sbounds.Max.Y
}
var scrollRange image.Rectangle
var scrollX, scrollY pointer.ScrollRange
textDims := e.text.FullDimensions()
visibleDims := e.text.Dimensions()
if e.SingleLine {
scrollOffX := e.text.ScrollOff().X
scrollRange.Min.X = min(-scrollOffX, 0)
scrollRange.Max.X = max(0, textDims.Size.X-(scrollOffX+visibleDims.Size.X))
scrollX.Min = min(-scrollOffX, 0)
scrollX.Max = max(0, textDims.Size.X-(scrollOffX+visibleDims.Size.X))
} else {
scrollOffY := e.text.ScrollOff().Y
scrollRange.Min.Y = -scrollOffY
scrollRange.Max.Y = max(0, textDims.Size.Y-(scrollOffY+visibleDims.Size.Y))
scrollY.Min = -scrollOffY
scrollY.Max = max(0, textDims.Size.Y-(scrollOffY+visibleDims.Size.Y))
}
sdist := e.scroller.Update(gtx.Metric, gtx.Source, gtx.Now, axis, scrollRange)
sdist := e.scroller.Update(gtx.Metric, gtx.Source, gtx.Now, axis, scrollX, scrollY)
var soff int
if e.SingleLine {
e.text.ScrollRel(sdist, 0)
@@ -289,6 +289,9 @@ func (e *Editor) processPointerEvent(gtx layout.Context, ev event.Event) (Editor
Y: int(math.Round(float64(evt.Position.Y))),
})
gtx.Execute(key.FocusCmd{Tag: e})
if !e.ReadOnly {
gtx.Execute(key.SoftKeyboardCmd{Show: true})
}
if e.scroller.State() != gesture.StateFlinging {
e.scrollCaret = true
}
@@ -312,8 +315,8 @@ func (e *Editor) processPointerEvent(gtx layout.Context, ev event.Event) (Editor
e.text.MoveWord(1, selectionExtend)
e.dragging = false
case evt.NumClicks >= 3:
e.text.MoveStart(selectionClear)
e.text.MoveEnd(selectionExtend)
e.text.MoveLineStart(selectionClear)
e.text.MoveLineEnd(selectionExtend)
e.dragging = false
}
}
@@ -374,8 +377,8 @@ func (e *Editor) processKey(gtx layout.Context) (EditorEvent, bool) {
key.Filter{Focus: e, Name: key.NameDeleteBackward, Optional: key.ModShortcutAlt | key.ModShift},
key.Filter{Focus: e, Name: key.NameDeleteForward, Optional: key.ModShortcutAlt | key.ModShift},
key.Filter{Focus: e, Name: key.NameHome, Optional: key.ModShift},
key.Filter{Focus: e, Name: key.NameEnd, Optional: key.ModShift},
key.Filter{Focus: e, Name: key.NameHome, Optional: key.ModShortcut | key.ModShift},
key.Filter{Focus: e, Name: key.NameEnd, Optional: key.ModShortcut | key.ModShift},
key.Filter{Focus: e, Name: key.NamePageDown, Optional: key.ModShift},
key.Filter{Focus: e, Name: key.NamePageUp, Optional: key.ModShift},
condFilter(!atBeginning, key.Filter{Focus: e, Name: key.NameLeftArrow, Optional: key.ModShortcutAlt | key.ModShift}),
@@ -395,7 +398,7 @@ func (e *Editor) processKey(gtx layout.Context) (EditorEvent, bool) {
case key.FocusEvent:
// Reset IME state.
e.ime.imeState = imeState{}
if ke.Focus {
if ke.Focus && !e.ReadOnly {
gtx.Execute(key.SoftKeyboardCmd{Show: true})
}
case key.Event:
@@ -521,6 +524,10 @@ func (e *Editor) command(gtx layout.Context, k key.Event) (EditorEvent, bool) {
}
}
}
case key.NameHome:
e.text.MoveTextStart(selAct)
case key.NameEnd:
e.text.MoveTextEnd(selAct)
}
return nil, false
}
@@ -582,9 +589,9 @@ func (e *Editor) command(gtx layout.Context, k key.Event) (EditorEvent, bool) {
case key.NamePageDown:
e.text.MovePages(+1, selAct)
case key.NameHome:
e.text.MoveStart(selAct)
e.text.MoveLineStart(selAct)
case key.NameEnd:
e.text.MoveEnd(selAct)
e.text.MoveLineEnd(selAct)
}
return nil, false
}
@@ -603,6 +610,7 @@ func (e *Editor) initBuffer() {
e.text.SingleLine = e.SingleLine
e.text.Mask = e.Mask
e.text.WrapPolicy = e.WrapPolicy
e.text.DisableSpaceTrim = true
}
// Update the state of the editor in response to input events. Update consumes editor
+77 -17
View File
@@ -256,7 +256,7 @@ func TestEditor(t *testing.T) {
// Regression test for bad in-cluster rune offset math.
e.SetText("æbc")
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
e.text.MoveEnd(selectionClear)
e.text.MoveLineEnd(selectionClear)
assertCaret(t, e, 0, 3, len("æbc"))
textSample := "æbc\naøå••"
@@ -268,7 +268,7 @@ func TestEditor(t *testing.T) {
}
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
assertCaret(t, e, 0, 0, 0)
e.text.MoveEnd(selectionClear)
e.text.MoveLineEnd(selectionClear)
assertCaret(t, e, 0, 3, len("æbc"))
e.MoveCaret(+1, +1)
assertCaret(t, e, 1, 0, len("æbc\n"))
@@ -276,7 +276,7 @@ func TestEditor(t *testing.T) {
assertCaret(t, e, 0, 3, len("æbc"))
e.text.MoveLines(+1, selectionClear)
assertCaret(t, e, 1, 4, len("æbc\naøå•"))
e.text.MoveEnd(selectionClear)
e.text.MoveLineEnd(selectionClear)
assertCaret(t, e, 1, 5, len("æbc\naøå••"))
e.MoveCaret(+1, +1)
assertCaret(t, e, 1, 5, len("æbc\naøå••"))
@@ -300,7 +300,7 @@ func TestEditor(t *testing.T) {
// Test that moveLine applies x offsets from previous moves.
e.SetText("long line\nshort")
e.SetCaret(0, 0)
e.text.MoveEnd(selectionClear)
e.text.MoveLineEnd(selectionClear)
e.text.MoveLines(+1, selectionClear)
e.text.MoveLines(-1, selectionClear)
assertCaret(t, e, 0, utf8.RuneCountInString("long line"), len("long line"))
@@ -342,14 +342,14 @@ func TestEditorRTL(t *testing.T) {
e.MoveCaret(+1, +1)
assertCaret(t, e, 0, 3, len("الح"))
// Move to the "end" of the line. This moves to the left edge of the line.
e.text.MoveEnd(selectionClear)
e.text.MoveLineEnd(selectionClear)
assertCaret(t, e, 0, 4, len("الحب"))
sentence := "الحب سماء لا\nتمط غير الأحلام"
e.SetText(sentence)
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
assertCaret(t, e, 0, 0, 0)
e.text.MoveEnd(selectionClear)
e.text.MoveLineEnd(selectionClear)
assertCaret(t, e, 0, 12, len("الحب سماء لا"))
e.MoveCaret(+1, +1)
assertCaret(t, e, 1, 0, len("الحب سماء لا\n"))
@@ -361,7 +361,7 @@ func TestEditorRTL(t *testing.T) {
assertCaret(t, e, 0, 12, len("الحب سماء لا"))
e.text.MoveLines(+1, selectionClear)
assertCaret(t, e, 1, 14, len("الحب سماء لا\nتمط غير الأحلا"))
e.text.MoveEnd(selectionClear)
e.text.MoveLineEnd(selectionClear)
assertCaret(t, e, 1, 15, len("الحب سماء لا\nتمط غير الأحلام"))
e.MoveCaret(+1, +1)
assertCaret(t, e, 1, 15, len("الحب سماء لا\nتمط غير الأحلام"))
@@ -417,7 +417,7 @@ func TestEditorLigature(t *testing.T) {
assertCaret(t, e, 0, 0, 0)
e.SetText("fl") // just a ligature
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
e.text.MoveEnd(selectionClear)
e.text.MoveLineEnd(selectionClear)
assertCaret(t, e, 0, 2, len("fl"))
e.MoveCaret(-1, -1)
assertCaret(t, e, 0, 1, len("f"))
@@ -428,7 +428,7 @@ func TestEditorLigature(t *testing.T) {
e.SetText("flaffl•ffi\n•fflfi") // 3 ligatures on line 0, 2 on line 1
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
assertCaret(t, e, 0, 0, 0)
e.text.MoveEnd(selectionClear)
e.text.MoveLineEnd(selectionClear)
assertCaret(t, e, 0, 10, len("ffaffl•ffi"))
e.MoveCaret(+1, +1)
assertCaret(t, e, 1, 0, len("ffaffl•ffi\n"))
@@ -481,7 +481,7 @@ func TestEditorLigature(t *testing.T) {
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
// Ensure that all runes in the final cluster of a line are properly
// decoded when moving to the end of the line. This is a regression test.
e.text.MoveEnd(selectionClear)
e.text.MoveLineEnd(selectionClear)
// The first line was broken by line wrapping, not a newline character, and has a trailing
// whitespace. However, we should never be able to reach the "other side" of such a trailing
// whitespace glyph.
@@ -548,8 +548,10 @@ const (
moveRune
moveLine
movePage
moveStart
moveEnd
moveTextStart
moveTextEnd
moveLineStart
moveLineEnd
moveCoord
moveWord
deleteWord
@@ -599,10 +601,14 @@ func TestEditorCaretConsistency(t *testing.T) {
e.text.MoveLines(int(distance), selectionClear)
case movePage:
e.text.MovePages(int(distance), selectionClear)
case moveStart:
e.text.MoveStart(selectionClear)
case moveEnd:
e.text.MoveEnd(selectionClear)
case moveLineStart:
e.text.MoveLineStart(selectionClear)
case moveLineEnd:
e.text.MoveLineEnd(selectionClear)
case moveTextStart:
e.text.MoveTextStart(selectionClear)
case moveTextEnd:
e.text.MoveTextEnd(selectionClear)
case moveCoord:
e.text.MoveCoord(image.Pt(int(x), int(y)))
case moveWord:
@@ -879,7 +885,7 @@ func (editMutation) Generate(rand *rand.Rand, size int) reflect.Value {
// to make it much narrower (which makes the lines in the editor reflow), and
// then verifies that the updated (col, line) positions of the selected text
// are where we expect.
func TestEditorSelect(t *testing.T) {
func TestEditorSelectReflow(t *testing.T) {
e := new(Editor)
e.SetText(`a 2 4 6 8 a
b 2 4 6 8 b
@@ -982,6 +988,60 @@ g 2 4 6 8 g
}
}
func TestEditorSelectShortcuts(t *testing.T) {
tFont := font.Font{}
tFontSize := unit.Sp(10)
tShaper := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
var tEditor = &Editor{
SingleLine: false,
ReadOnly: true,
}
lines := "abc abc abc\ndef def def\nghi ghi ghi"
tEditor.SetText(lines)
type testCase struct {
// Initial text selection.
startPos, endPos int
// Keyboard shortcut to execute.
keyEvent key.Event
// Expected text selection.
selection string
}
pos1, pos2 := 14, 21
for n, tst := range []testCase{
{pos1, pos2, key.Event{Name: "A", Modifiers: key.ModShortcut}, lines},
{pos2, pos1, key.Event{Name: "A", Modifiers: key.ModShortcut}, lines},
{pos1, pos2, key.Event{Name: key.NameHome, Modifiers: key.ModShift}, "def def d"},
{pos1, pos2, key.Event{Name: key.NameEnd, Modifiers: key.ModShift}, "ef"},
{pos2, pos1, key.Event{Name: key.NameHome, Modifiers: key.ModShift}, "de"},
{pos2, pos1, key.Event{Name: key.NameEnd, Modifiers: key.ModShift}, "f def def"},
{pos1, pos2, key.Event{Name: key.NameHome, Modifiers: key.ModShortcut | key.ModShift}, "abc abc abc\ndef def d"},
{pos1, pos2, key.Event{Name: key.NameEnd, Modifiers: key.ModShortcut | key.ModShift}, "ef\nghi ghi ghi"},
{pos2, pos1, key.Event{Name: key.NameHome, Modifiers: key.ModShortcut | key.ModShift}, "abc abc abc\nde"},
{pos2, pos1, key.Event{Name: key.NameEnd, Modifiers: key.ModShortcut | key.ModShift}, "f def def\nghi ghi ghi"},
} {
tRouter := new(input.Router)
gtx := layout.Context{
Ops: new(op.Ops),
Locale: english,
Constraints: layout.Exact(image.Pt(100, 100)),
Source: tRouter.Source(),
}
gtx.Execute(key.FocusCmd{Tag: tEditor})
tEditor.Layout(gtx, tShaper, tFont, tFontSize, op.CallOp{}, op.CallOp{})
tEditor.SetCaret(tst.startPos, tst.endPos)
if cStart, cEnd := tEditor.Selection(); cStart != tst.startPos || cEnd != tst.endPos {
t.Errorf("TestEditorSelect %d: initial selection", n)
}
tRouter.Queue(tst.keyEvent)
tEditor.Update(gtx)
if got := tEditor.SelectedText(); got != tst.selection {
t.Errorf("TestEditorSelect %d: Expected %q, got %q", n, tst.selection, got)
}
}
}
// Verify that an existing selection is dismissed when you press arrow keys.
func TestSelectMove(t *testing.T) {
e := new(Editor)
+65 -47
View File
@@ -2,12 +2,18 @@ package widget
import (
"bytes"
"image"
"image/png"
"io"
"os"
"testing"
nsareg "eliasnaur.com/font/noto/sans/arabic/regular"
"gioui.org/font"
"gioui.org/font/opentype"
"gioui.org/gpu/headless"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/text"
"golang.org/x/image/font/gofont/goregular"
"golang.org/x/image/math/fixed"
@@ -16,11 +22,11 @@ import (
// makePosTestText returns two bidi samples of shaped text at the given
// font size and wrapped to the given line width. The runeLimit, if nonzero,
// truncates the sample text to ensure shorter output for expensive tests.
func makePosTestText(fontSize, lineWidth int, alignOpposite bool) (source string, bidiLTR, bidiRTL []text.Glyph) {
func makePosTestText(fontSize, lineWidth int, alignOpposite bool) (shaper *text.Shaper, source string, bidiLTR, bidiRTL []text.Glyph) {
ltrFace, _ := opentype.Parse(goregular.TTF)
rtlFace, _ := opentype.Parse(nsareg.TTF)
shaper := text.NewShaper(text.NoSystemFonts(), text.WithCollection([]font.FontFace{
shaper = text.NewShaper(text.NoSystemFonts(), text.WithCollection([]font.FontFace{
{
Font: font.Font{Typeface: "LTR"},
Face: ltrFace,
@@ -58,7 +64,7 @@ func makePosTestText(fontSize, lineWidth int, alignOpposite bool) (source string
for g, ok := shaper.NextGlyph(); ok; g, ok = shaper.NextGlyph() {
bidiRTL = append(bidiRTL, g)
}
return bidiSource, bidiLTR, bidiRTL
return shaper, bidiSource, bidiLTR, bidiRTL
}
// makeAccountingTestText shapes text designed to stress rune accounting
@@ -260,7 +266,7 @@ func TestIndexPositionWhitespace(t *testing.T) {
func TestIndexPositionBidi(t *testing.T) {
fontSize := 16
lineWidth := fontSize * 10
_, bidiLTRText, bidiRTLText := makePosTestText(fontSize, lineWidth, false)
shaper, _, bidiLTRText, bidiRTLText := makePosTestText(fontSize, lineWidth, false)
type testcase struct {
name string
glyphs []text.Glyph
@@ -284,13 +290,12 @@ func TestIndexPositionBidi(t *testing.T) {
name: "bidi rtl",
glyphs: bidiRTLText,
expectedXs: []fixed.Int26_6{
2646, 3272, 3842, 4412, 4697, 5267, 5837, 6090, 6602, 7114, 2646, 2380, 1577, 985, 687, 266, // Positions on line 0.
7867, 7099, 6331, 5563, 4795, 4510, 4212, 3914, 3648, 2281, 2566, 3136, 3648, 2281, 2015, 1709, 1117, 266, // Positions on line 1.
8794, 8026, 7258, 6490, 5722, 5437, 4922, 4540, 4134, 3868, 0, 290, 860, 1430, 1715, 1989, 2559, 3071, 3583, // Positions on line 2.
324, 894, 1464, 2034, 324, 0, // Positions on line 3.
// Line 0
5718, 6344, 6914, 7484, 7769, 8339, 8909, 9162, 9674, 10186, 5718, 5452, 4649, 4057, 3759, 3338, 3072, 2304, 1536, 768, 0,
// Line 1
9170, 8872, 8574, 8308, 6941, 7226, 7796, 8308, 6941, 6675, 6369, 5777, 4926, 4660, 3892, 3124, 2356, 1588, 1303, 788, 406, 0,
// Line 2
324, 614, 1184, 1754, 2039, 2313, 2883, 3395, 3907, 4192, 4762, 5332, 5902, 324, 0,
},
},
} {
@@ -337,6 +342,35 @@ func TestIndexPositionBidi(t *testing.T) {
printPositions(t, gi.positions)
if t.Failed() {
printGlyphs(t, tc.glyphs)
width := lineWidth
height := 100
cap := image.NewRGBA(image.Rect(0, 0, width, height))
w, _ := headless.NewWindow(width, height)
defer w.Release()
ops := new(op.Ops)
gtx := layout.Context{
Constraints: layout.Constraints{Max: image.Pt(width, height)},
Ops: ops,
}
it := textIterator{viewport: image.Rectangle{Max: image.Point{X: width, Y: height}}}
for _, g := range tc.glyphs {
it.processGlyph(g, true)
}
var glyphs [32]text.Glyph
line := glyphs[:0]
for _, g := range gi.glyphs {
var ok bool
if line, ok = it.paintGlyph(gtx, shaper, g, line); !ok {
break
}
}
w.Frame(ops)
w.Screenshot(cap)
b := new(bytes.Buffer)
_ = png.Encode(b, cap)
screenshotName := tc.name + ".png"
_ = os.WriteFile(screenshotName, b.Bytes(), 0o644)
t.Logf("wrote %q", screenshotName)
}
})
}
@@ -345,8 +379,8 @@ func TestIndexPositionBidi(t *testing.T) {
func TestIndexPositionLines(t *testing.T) {
fontSize := 16
lineWidth := fontSize * 10
source1, bidiLTRText, bidiRTLText := makePosTestText(fontSize, lineWidth, false)
source2, bidiLTRTextOpp, bidiRTLTextOpp := makePosTestText(fontSize, lineWidth, true)
_, source1, bidiLTRText, bidiRTLText := makePosTestText(fontSize, lineWidth, false)
_, source2, bidiLTRTextOpp, bidiRTLTextOpp := makePosTestText(fontSize, lineWidth, true)
type testcase struct {
name string
source string
@@ -379,7 +413,7 @@ func TestIndexPositionLines(t *testing.T) {
xOff: fixed.Int26_6(0),
yOff: 60,
glyphs: 18,
width: fixed.Int26_6(8813),
width: fixed.Int26_6(8528),
ascent: fixed.Int26_6(1407),
descent: fixed.Int26_6(756),
},
@@ -401,32 +435,24 @@ func TestIndexPositionLines(t *testing.T) {
{
xOff: fixed.Int26_6(0),
yOff: 22,
glyphs: 15,
width: fixed.Int26_6(7114),
glyphs: 20,
width: fixed.Int26_6(10186),
ascent: fixed.Int26_6(1407),
descent: fixed.Int26_6(756),
},
{
xOff: fixed.Int26_6(0),
yOff: 41,
glyphs: 15,
width: fixed.Int26_6(7867),
glyphs: 19,
width: fixed.Int26_6(9170),
ascent: fixed.Int26_6(1407),
descent: fixed.Int26_6(756),
},
{
xOff: fixed.Int26_6(0),
yOff: 60,
glyphs: 18,
width: fixed.Int26_6(8794),
ascent: fixed.Int26_6(1407),
descent: fixed.Int26_6(756),
},
{
xOff: fixed.Int26_6(0),
yOff: 79,
glyphs: 4,
width: fixed.Int26_6(2034),
glyphs: 13,
width: fixed.Int26_6(5902),
ascent: fixed.Int26_6(968),
descent: fixed.Int26_6(216),
},
@@ -454,10 +480,10 @@ func TestIndexPositionLines(t *testing.T) {
descent: fixed.Int26_6(756),
},
{
xOff: fixed.Int26_6(1427),
xOff: fixed.Int26_6(1712),
yOff: 60,
glyphs: 18,
width: fixed.Int26_6(8813),
width: fixed.Int26_6(8528),
ascent: fixed.Int26_6(1407),
descent: fixed.Int26_6(756),
},
@@ -477,34 +503,26 @@ func TestIndexPositionLines(t *testing.T) {
glyphs: bidiRTLTextOpp,
expectedLines: []lineInfo{
{
xOff: fixed.Int26_6(3126),
xOff: fixed.Int26_6(54),
yOff: 22,
glyphs: 15,
width: fixed.Int26_6(7114),
glyphs: 20,
width: fixed.Int26_6(10186),
ascent: fixed.Int26_6(1407),
descent: fixed.Int26_6(756),
},
{
xOff: fixed.Int26_6(2373),
xOff: fixed.Int26_6(1070),
yOff: 41,
glyphs: 15,
width: fixed.Int26_6(7867),
glyphs: 19,
width: fixed.Int26_6(9170),
ascent: fixed.Int26_6(1407),
descent: fixed.Int26_6(756),
},
{
xOff: fixed.Int26_6(1446),
xOff: fixed.Int26_6(4338),
yOff: 60,
glyphs: 18,
width: fixed.Int26_6(8794),
ascent: fixed.Int26_6(1407),
descent: fixed.Int26_6(756),
},
{
xOff: fixed.Int26_6(8206),
yOff: 79,
glyphs: 4,
width: fixed.Int26_6(2034),
glyphs: 13,
width: fixed.Int26_6(5902),
ascent: fixed.Int26_6(968),
descent: fixed.Int26_6(216),
},
+1 -1
View File
@@ -75,7 +75,7 @@ func (d DecorationsStyle) layoutDecorations(gtx layout.Context) layout.Dimension
case system.ActionMinimize:
w = minimizeWindow
case system.ActionMaximize:
if d.Decorations.Maximized() {
if d.Decorations.Maximized {
w = maximizedWindow
} else {
w = maximizeWindow
+3
View File
@@ -54,6 +54,9 @@ func (p ProgressBarStyle) Layout(gtx layout.Context) layout.Dimensions {
if !gtx.Enabled() {
fillColor = f32color.Disabled(fillColor)
}
if fillWidth < int(p.Radius*2) {
fillWidth = int(p.Radius * 2)
}
return shader(fillWidth, fillColor)
}),
)
+3
View File
@@ -32,6 +32,9 @@ type Palette struct {
ContrastFg color.NRGBA
}
// Theme holds the general theme of an app or window. Different top-level
// windows should have different instances of Theme (with different Shapers;
// see the godoc for [text.Shaper]), though their other fields can be equal.
type Theme struct {
Shaper *text.Shaper
Palette
+4 -4
View File
@@ -247,8 +247,8 @@ func (e *Selectable) processPointer(gtx layout.Context) {
e.text.MoveWord(1, selectionExtend)
e.dragging = false
case evt.NumClicks >= 3:
e.text.MoveStart(selectionClear)
e.text.MoveEnd(selectionExtend)
e.text.MoveLineStart(selectionClear)
e.text.MoveLineEnd(selectionExtend)
e.dragging = false
}
}
@@ -378,9 +378,9 @@ func (e *Selectable) command(gtx layout.Context, k key.Event) {
case key.NamePageDown:
e.text.MovePages(+1, selAct)
case key.NameHome:
e.text.MoveStart(selAct)
e.text.MoveLineStart(selAct)
case key.NameEnd:
e.text.MoveEnd(selAct)
e.text.MoveLineEnd(selAct)
}
}
+30 -4
View File
@@ -61,6 +61,9 @@ type textView struct {
Truncator string
// WrapPolicy configures how displayed text will be broken into lines.
WrapPolicy text.WrapPolicy
// DisableSpaceTrim configures whether trailing whitespace on a line will have its
// width zeroed. Set to true for editors, but false for non-editable text.
DisableSpaceTrim bool
// Mask replaces the visual display of each rune in the contents with the given rune.
// Newline characters are not masked. When non-zero, the unmasked contents
// are accessed by Len, Text, and SetText.
@@ -285,6 +288,10 @@ func (e *textView) Layout(gtx layout.Context, lt *text.Shaper, font font.Font, s
e.params.LineHeightScale = e.LineHeightScale
e.invalidate()
}
if e.DisableSpaceTrim != e.params.DisableSpaceTrim {
e.params.DisableSpaceTrim = e.DisableSpaceTrim
e.invalidate()
}
e.makeValid()
@@ -639,9 +646,28 @@ func (e *textView) MoveCaret(startDelta, endDelta int) {
e.caret.end = e.moveByGraphemes(e.caret.end, endDelta)
}
// MoveStart moves the caret to the start of the current line, ensuring that the resulting
// MoveTextStart moves the caret to the start of the text.
func (e *textView) MoveTextStart(selAct selectionAction) {
caret := e.closestToRune(e.caret.end)
e.caret.start = 0
e.caret.end = caret.runes
e.caret.xoff = -caret.x
e.updateSelection(selAct)
e.clampCursorToGraphemes()
}
// MoveTextEnd moves the caret to the end of the text.
func (e *textView) MoveTextEnd(selAct selectionAction) {
caret := e.closestToRune(math.MaxInt)
e.caret.start = caret.runes
e.caret.xoff = fixed.I(e.params.MaxWidth) - caret.x
e.updateSelection(selAct)
e.clampCursorToGraphemes()
}
// MoveLineStart moves the caret to the start of the current line, ensuring that the resulting
// cursor position is on a grapheme cluster boundary.
func (e *textView) MoveStart(selAct selectionAction) {
func (e *textView) MoveLineStart(selAct selectionAction) {
caret := e.closestToRune(e.caret.start)
caret = e.closestToLineCol(caret.lineCol.line, 0)
e.caret.start = caret.runes
@@ -650,9 +676,9 @@ func (e *textView) MoveStart(selAct selectionAction) {
e.clampCursorToGraphemes()
}
// MoveEnd moves the caret to the end of the current line, ensuring that the resulting
// MoveLineEnd moves the caret to the end of the current line, ensuring that the resulting
// cursor position is on a grapheme cluster boundary.
func (e *textView) MoveEnd(selAct selectionAction) {
func (e *textView) MoveLineEnd(selAct selectionAction) {
caret := e.closestToRune(e.caret.start)
caret = e.closestToLineCol(caret.lineCol.line, math.MaxInt)
e.caret.start = caret.runes