mirror of
https://github.com/bitsycore/compose-desktop-native.git
synced 2026-10-06 19:27:26 +00:00
Compare commits
10
Commits
0c09109e6e
...
eb44db915e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb44db915e | ||
|
|
1120abc488 | ||
|
|
9d2656f905 | ||
|
|
48cfa5da4d | ||
|
|
856cbe5ef3 | ||
|
|
5c7308c614 | ||
|
|
2fb322776a | ||
|
|
7c7bb63cae | ||
|
|
02fb2ddf4e | ||
|
|
126e83c056 |
@@ -566,8 +566,18 @@ drift / vendor-clean guardrails are in
|
|||||||
## Key files by area - start here when you need to find something
|
## Key files by area - start here when you need to find something
|
||||||
|
|
||||||
### Renderer + main loop
|
### Renderer + main loop
|
||||||
- `compose/desktop/native/desktop-native-window/src/nativeMain/…/ComposeWindow.kt` - main loop,
|
- `compose/desktop/native/desktop-native-window/src/nativeMain/…/` - the window
|
||||||
recomposer lifecycle, SDL event dispatch, composition-local seeding.
|
module, one concern per file:
|
||||||
|
- `ComposeWindow.kt` - the PUBLIC surface (`ApplicationScope`, `Window()`,
|
||||||
|
`nativeComposeApp`, `nativeComposeWindow`) + the main loop.
|
||||||
|
- `WindowInstance.kt` - one SDL window: backend + render backend + root host +
|
||||||
|
its own recomposer/composition, event handling, frame pump, teardown. This is
|
||||||
|
where composition-local seeding lives.
|
||||||
|
- `AppRuntime.kt` - the live-window registry + create/destroy lifecycle.
|
||||||
|
- `WindowAttributes.kt` - the Compose Desktop `Window()` attribute bundle.
|
||||||
|
- `FrameTiming.kt` - deterministic/virtual frame clocks and the
|
||||||
|
probe-quiescence hooks (`disableInfiniteAnimations`, `useVirtualFrameTime`,
|
||||||
|
`windowHasInvalidations`).
|
||||||
- `compose/ui/ui/src/nativeMain/…/RenderBackend.kt` - the interface.
|
- `compose/ui/ui/src/nativeMain/…/RenderBackend.kt` - the interface.
|
||||||
- `compose/ui/ui/src/nativeMain/…/GpuMode.kt` - sealed driver picker
|
- `compose/ui/ui/src/nativeMain/…/GpuMode.kt` - sealed driver picker
|
||||||
(`Auto` / `Software` / `Skia.OpenGL` / `Skia.Metal`).
|
(`Auto` / `Software` / `Skia.OpenGL` / `Skia.Metal`).
|
||||||
@@ -632,9 +642,23 @@ Open renderer / fidelity / release work is tracked in [PLAN.md](PLAN.md).
|
|||||||
|
|
||||||
## Conventions
|
## Conventions
|
||||||
|
|
||||||
Kotlin standard style - plain `camelCase` for parameters, local variables,
|
Kotlin standard style for formatting (braces, wrapping, indentation).
|
||||||
and fields. **No `f`/`in`/`v` prefixes**, no SPIRTECH scheme (see the global
|
|
||||||
CLAUDE.md - that scheme is C-only).
|
Naming, as the code actually is - this paragraph used to claim "no prefixes",
|
||||||
|
which had not been true for a long time:
|
||||||
|
|
||||||
|
- **`m` for private/protected members** - `mWidth`, `mSurface`, `mCache`. NOT
|
||||||
|
`f`: the `f` prefix was retired across the project, so do not reintroduce it.
|
||||||
|
- **`v` for local variables** - `vWindow`, `vResult`.
|
||||||
|
- **`in` for function parameters** - `inTitle`, `inWidth`. Note Compose
|
||||||
|
`@Composable` parameters are the exception: they stay plain, because the name
|
||||||
|
is part of the call-site API (`title = `, `onCloseRequest = `).
|
||||||
|
- **`k` for private file-level constants** - `kMaxFrames`, `kIsWindows`.
|
||||||
|
- Public API surface is plain `camelCase` - prefixes are for implementation
|
||||||
|
detail only, and nothing prefixed should ever reach the klib dump.
|
||||||
|
|
||||||
|
VENDORED code under `src/vendor/` follows UPSTREAM's naming, untouched - it is
|
||||||
|
copied byte-for-byte, so never re-prefix anything in it.
|
||||||
|
|
||||||
Section headers inside a file, when useful:
|
Section headers inside a file, when useful:
|
||||||
|
|
||||||
@@ -752,7 +776,7 @@ Caveats that make these work here (all already wired - listed so nobody
|
|||||||
- The google `LocalViewModelStoreOwner` / `LocalSavedStateRegistryOwner` /
|
- The google `LocalViewModelStoreOwner` / `LocalSavedStateRegistryOwner` /
|
||||||
`LocalLifecycleOwner` are PLAIN composition locals - the JB HostDefault
|
`LocalLifecycleOwner` are PLAIN composition locals - the JB HostDefault
|
||||||
mechanism (`compositionLocalWithHostDefaultOf`) does not exist in google
|
mechanism (`compositionLocalWithHostDefaultOf`) does not exist in google
|
||||||
artifacts. `WindowArchitectureOwner` (ComposeWindow.kt) provides all three
|
artifacts. `WindowArchitectureOwner` (WindowInstance.kt) provides all three
|
||||||
per window, mirrors upstream desktop's DefaultArchitectureComponentsOwner,
|
per window, mirrors upstream desktop's DefaultArchitectureComponentsOwner,
|
||||||
calls `enableSavedStateHandles()` at construction, and follows SDL focus /
|
calls `enableSavedStateHandles()` at construction, and follows SDL focus /
|
||||||
visibility (focused → RESUMED, unfocused → STARTED, minimised → CREATED).
|
visibility (focused → RESUMED, unfocused → STARTED, minimised → CREATED).
|
||||||
|
|||||||
@@ -391,21 +391,21 @@ re-vendoring.** The debt is **completing native-actual stubs**.
|
|||||||
### P0 - blocks fidelity/correctness
|
### P0 - blocks fidelity/correctness
|
||||||
|
|
||||||
- [~] **P0** Text context menu (right-click copy/paste/select-all) - **RECONCILED: already
|
- [~] **P0** Text context menu (right-click copy/paste/select-all) - **RECONCILED: already
|
||||||
works, no reimplementation.** Static trace confirmed the menu is wired end-to-end
|
works, no reimplementation.** Static trace confirmed the menu is wired end-to-end
|
||||||
through the vendored LEGACY path: text-level `ContextMenuArea` (`ContextMenu.native.kt`)
|
through the vendored LEGACY path: text-level `ContextMenuArea` (`ContextMenu.native.kt`)
|
||||||
→ `CommonContextMenuArea` (`vendor/common/.../text/CommonContextMenuArea.kt`) → (native
|
→ `CommonContextMenuArea` (`vendor/common/.../text/CommonContextMenuArea.kt`) → (native
|
||||||
`ComposeFoundationFlags.isNewContextMenuEnabled = false`, so the legacy branch) →
|
`ComposeFoundationFlags.isNewContextMenuEnabled = false`, so the legacy branch) →
|
||||||
`contextmenu.ContextMenuArea` → `contextMenuGestures`/`onRightClickDown`
|
`contextmenu.ContextMenuArea` → `contextMenuGestures`/`onRightClickDown`
|
||||||
(`isSecondaryPressed`) → `ContextMenuPopup` → `Popup.native.kt` (hosted by
|
(`isSecondaryPressed`) → `ContextMenuPopup` → `Popup.native.kt` (hosted by
|
||||||
`LocalPopupHost` at the app root). Items are real: `TextFieldSelectionManager` /
|
`LocalPopupHost` at the app root). Items are real: `TextFieldSelectionManager` /
|
||||||
`SelectionManager.contextMenuBuilder` emit Cut/Copy/Paste/SelectAll with real actions.
|
`SelectionManager.contextMenuBuilder` emit Cut/Copy/Paste/SelectAll with real actions.
|
||||||
SDL right-click → `PointerButton.Secondary` (`SDL3EventMapper.kt`) →
|
SDL right-click → `PointerButton.Secondary` (`AppEvent.kt`) →
|
||||||
`isSecondaryPressed` (`PointerEventBridge`). The three "NOP" seams belong to the
|
`isSecondaryPressed` (`PointerEventBridge`). The three "NOP" seams belong to the
|
||||||
DISABLED *new* context-menu API and are unreachable - **DONE:** their misleading
|
DISABLED *new* context-menu API and are unreachable - **DONE:** their misleading
|
||||||
`TODO(CMP-7819)` comments corrected to say so, in all three files.
|
`TODO(CMP-7819)` comments corrected to say so, in all three files.
|
||||||
**REMAINING:** an interactive right-click smoke check (no headless driver here - fold
|
**REMAINING:** an interactive right-click smoke check (no headless driver here - fold
|
||||||
into manual/WIN-SMOKE). Known limitation, not a breakage: Paste enablement is
|
into manual/WIN-SMOKE). Known limitation, not a breakage: Paste enablement is
|
||||||
plain-text-only (`ClipboardPasteState.hasClip = hasText`, see P2 below).
|
plain-text-only (`ClipboardPasteState.hasClip = hasText`, see P2 below).
|
||||||
- [ ] **P1** DatePicker/TimePicker localization - VERIFIED current state:
|
- [ ] **P1** DatePicker/TimePicker localization - VERIFIED current state:
|
||||||
`material3/.../internal/PlatformDateFormat.native.kt` is a real kotlinx-datetime
|
`material3/.../internal/PlatformDateFormat.native.kt` is a real kotlinx-datetime
|
||||||
formatter that DOES honor CLDR patterns/skeletons (renders "Jul 29, 2026" /
|
formatter that DOES honor CLDR patterns/skeletons (renders "Jul 29, 2026" /
|
||||||
@@ -417,14 +417,14 @@ re-vendoring.** The debt is **completing native-actual stubs**.
|
|||||||
(author):** bundle a CLDR subset or K/N i18n lib for localized names + locale-aware
|
(author):** bundle a CLDR subset or K/N i18n lib for localized names + locale-aware
|
||||||
first-day/24h; also unblocks `CalendarLocale.native.kt:20` (fixed `"en"`). Ships
|
first-day/24h; also unblocks `CalendarLocale.native.kt:20` (fixed `"en"`). Ships
|
||||||
readable English dates today - polish, not a P0 gate.
|
readable English dates today - polish, not a P0 gate.
|
||||||
- [x] **P0** Float pointer coordinates - `SDL3EventMapper.kt` `.toInt()`-truncated SDL's
|
- [x] **P0** Float pointer coordinates - `AppEvent.kt` `.toInt()`-truncated SDL's
|
||||||
Float `mb.x`/`mb.y`/`mm.x`/`mm.y` and the wheel `mouse_x/y` before the DPR multiply in
|
Float `mb.x`/`mb.y`/`mm.x`/`mm.y` and the wheel `mouse_x/y` before the DPR multiply in
|
||||||
`ComposeWindow.kt`. On 2× displays a click at logical 100.9 → physical 200 not ~201,
|
`ComposeWindow.kt`. On 2× displays a click at logical 100.9 → physical 200 not ~201,
|
||||||
quantizing the caret to 2px steps near glyph edges. **DONE:** widened
|
quantizing the caret to 2px steps near glyph edges. **DONE:** widened
|
||||||
`LegacyPointerEvent.x/y` and `AppEvent.MouseWheel.x/y` to `Float`, dropped the
|
`LegacyPointerEvent.x/y` and `AppEvent.MouseWheel.x/y` to `Float`, dropped the
|
||||||
`.toInt()` at the three mapper construction sites, and removed the now-redundant
|
`.toInt()` at the three mapper construction sites, and removed the now-redundant
|
||||||
`.toFloat()` at the two `ComposeWindow` read sites. Consumers fully contained (verified
|
`.toFloat()` at the two `ComposeWindow` read sites. Consumers fully contained (verified
|
||||||
by grep). Builds + runs clean on macosArm64.
|
by grep). Builds + runs clean on macosArm64.
|
||||||
|
|
||||||
### P1 - quality / parity
|
### P1 - quality / parity
|
||||||
|
|
||||||
@@ -521,7 +521,7 @@ re-vendoring.** The debt is **completing native-actual stubs**.
|
|||||||
Static lib built from source per host (`scripts/build-sdl/build-all.py`, ref
|
Static lib built from source per host (`scripts/build-sdl/build-all.py`, ref
|
||||||
`release-3.4.12`); linked into the exe via `sdl3.def` (`staticLibraries=libSDL3.a`). Today
|
`release-3.4.12`); linked into the exe via `sdl3.def` (`staticLibraries=libSDL3.a`). Today
|
||||||
only tests/examples + Windows D3D12/GPU are off (`build-all.py:311-319`). `SDL_Init` uses
|
only tests/examples + Windows D3D12/GPU are off (`build-all.py:311-319`). `SDL_Init` uses
|
||||||
**`SDL_INIT_VIDEO` only** (`SDL3Backend.kt:49`).
|
**`SDL_INIT_VIDEO` only** (`Sdl3Backend.kt:49`).
|
||||||
|
|
||||||
**USED (keep ON):** Video/window, Events, Clipboard, Dialog (file open/save - no cheaper
|
**USED (keep ON):** Video/window, Events, Clipboard, Dialog (file open/save - no cheaper
|
||||||
substitute; Linux uses portal/zenity, keep deps), OpenGL + Metal contexts, Render (2D CPU-
|
substitute; Linux uses portal/zenity, keep deps), OpenGL + Metal contexts, Render (2D CPU-
|
||||||
@@ -554,7 +554,7 @@ joystick.
|
|||||||
disabled SDL subsystems.)
|
disabled SDL subsystems.)
|
||||||
- [ ] **P2** (higher-risk, flag-don't-apply) Render-driver pruning to software-only. Only
|
- [ ] **P2** (higher-risk, flag-don't-apply) Render-driver pruning to software-only. Only
|
||||||
the CPU-raster fallback uses `SDL_Render`; GL/Metal go direct. But
|
the CPU-raster fallback uses `SDL_Render`; GL/Metal go direct. But
|
||||||
`SDL_CreateRenderer(window, null)` (`SDL3Backend.kt:117`) lets SDL pick the first
|
`SDL_CreateRenderer(window, null)` (`Sdl3Backend.kt:117`) lets SDL pick the first
|
||||||
driver - with only software present it picks software (fine) but couples the fallback
|
driver - with only software present it picks software (fine) but couples the fallback
|
||||||
to that assumption. **Conservative: leave render drivers alone.**
|
to that assumption. **Conservative: leave render drivers alone.**
|
||||||
|
|
||||||
@@ -727,6 +727,30 @@ A read-only feasibility map (no code changed) found two layers:
|
|||||||
proves infeasible, fall back to keeping the seam but at least converting the hand files to tracked
|
proves infeasible, fall back to keeping the seam but at least converting the hand files to tracked
|
||||||
Rule-3 vendors (like `FontRasterizationSettings.native.kt` now has a `VENDOR-BASE` line).
|
Rule-3 vendors (like `FontRasterizationSettings.native.kt` now has a `VENDOR-BASE` line).
|
||||||
|
|
||||||
|
### 7c. `demo --multiwintest` segfault on mingwX64 - ✅ FIXED
|
||||||
|
|
||||||
|
The multi-window probe exited 139 with no output, so it had silently been
|
||||||
|
asserting nothing. Root cause: an OpenGL context is per-WINDOW but "current" is
|
||||||
|
per-THREAD, and `SDL_GL_MakeCurrent` ran only once at creation
|
||||||
|
(`Sdl3Backend.init`) with nothing re-binding it per frame. With two windows open,
|
||||||
|
whichever initialised last owned the thread - so window A issued its draw and
|
||||||
|
swap against window B's context (silent corruption while both lived), and
|
||||||
|
destroying B left the current context dangling, faulting in `present()`.
|
||||||
|
|
||||||
|
Fixed in `SkiaGLBridge` by binding the window's own context at every entry point
|
||||||
|
that touches GL: `init`, `ensureSize` (before its unchanged-size early return -
|
||||||
|
this is the per-frame bind that covers the draw phase), `present`, and `destroy`
|
||||||
|
(so a window frees its GPU objects against its own context). Purely additive, 29
|
||||||
|
lines.
|
||||||
|
|
||||||
|
Single-window rendering is bit-identical before and after (same mean RGB
|
||||||
|
[46.6 44.4 51.5] and 167 distinct colours on the Buttons screenshot), which is
|
||||||
|
expected: SDL short-circuits a redundant bind. `--multiwintest` now PASSes.
|
||||||
|
|
||||||
|
NOTE the multi-window path was never correct, not just at teardown - two live
|
||||||
|
windows were sharing one context. Anything that looked like flicker or
|
||||||
|
cross-window corruption before this is explained by it.
|
||||||
|
|
||||||
## Accepted 1.0.0 gaps (documented, not fixed)
|
## Accepted 1.0.0 gaps (documented, not fixed)
|
||||||
|
|
||||||
Drag-OUT of window (SDL platform limit; drop-IN works), full accessibility pipeline (out of
|
Drag-OUT of window (SDL platform limit; drop-IN works), full accessibility pipeline (out of
|
||||||
|
|||||||
@@ -71,6 +71,23 @@ Each window gets its own `Lifecycle`, `ViewModelStore`, and `SavedStateRegistry`
|
|||||||
owners, driven by real SDL focus and visibility events, so ViewModels and
|
owners, driven by real SDL focus and visibility events, so ViewModels and
|
||||||
saved state behave as they do on Android.
|
saved state behave as they do on Android.
|
||||||
|
|
||||||
|
`Window()` takes the same attributes as Compose Desktop's - `undecorated`,
|
||||||
|
`transparent`, `resizable`, `enabled`, `focusable`, `alwaysOnTop`,
|
||||||
|
`onPreviewKeyEvent` and `onKeyEvent` - and re-applies them when they change.
|
||||||
|
(`transparent` is the one exception: SDL needs it at window-creation time, so it
|
||||||
|
is fixed for the window's life.)
|
||||||
|
|
||||||
|
When the Compose-level API doesn't reach far enough, drop to SDL directly:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
val handles = window.rawSdlHandles() // SDL_Window* / SDL_Renderer* / GL / Metal
|
||||||
|
handles.window?.let { sdl3.SDL_FlashWindow(it.reinterpret(), SDL_FLASH_BRIEFLY) }
|
||||||
|
```
|
||||||
|
|
||||||
|
The pointers are a snapshot valid only while the window lives, and which ones
|
||||||
|
are non-null depends on the resolved renderer (`glContext` for Skia OpenGL,
|
||||||
|
`metalView` for Metal, `renderer` for CPU raster).
|
||||||
|
|
||||||
### Building in this repo
|
### Building in this repo
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
@@ -256,17 +273,6 @@ mirrors or you will have every class twice.
|
|||||||
| Collection | `androidx.collection:collection:1.5.0` |
|
| Collection | `androidx.collection:collection:1.5.0` |
|
||||||
| Graphics Shapes | `androidx.graphics:graphics-shapes:1.1.0` |
|
| Graphics Shapes | `androidx.graphics:graphics-shapes:1.1.0` |
|
||||||
|
|
||||||
### Infrastructure
|
|
||||||
|
|
||||||
| Library | Version |
|
|
||||||
|---|---|
|
|
||||||
| kotlinx-coroutines | 1.11.0 |
|
|
||||||
| kotlinx-serialization | 1.11.0 |
|
|
||||||
| kotlinx-datetime | 0.8.0 |
|
|
||||||
| kotlinx-io-okio | 0.9.1 |
|
|
||||||
| Okio | 3.18.2 |
|
|
||||||
| Ktor | 3.5.2 |
|
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
Build the native libraries once per machine, then build any app target:
|
Build the native libraries once per machine, then build any app target:
|
||||||
@@ -281,6 +287,10 @@ command line tools; Linux needs gcc/g++ and the X11 / Wayland / audio dev
|
|||||||
headers; Windows needs a mingw-w64 g++ on PATH. See [TOOLING.md](TOOLING.md) for
|
headers; Windows needs a mingw-w64 g++ on PATH. See [TOOLING.md](TOOLING.md) for
|
||||||
the full build and verification workflow.
|
the full build and verification workflow.
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
[Bitsycore's Discord](https://discord.gg/kGnraVAtDR)
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
- [TOOLING.md](TOOLING.md): building the native libs, vendoring, and verification.
|
- [TOOLING.md](TOOLING.md): building the native libs, vendoring, and verification.
|
||||||
|
|||||||
@@ -56,11 +56,11 @@ internal class PackState(
|
|||||||
// A linked copy mirrors its source's requests read-only; non-linked packs own
|
// A linked copy mirrors its source's requests read-only; non-linked packs own
|
||||||
// their requests. `requests` resolves to whichever applies.
|
// their requests. `requests` resolves to whichever applies.
|
||||||
var linkedSource by mutableStateOf<PackState?>(null)
|
var linkedSource by mutableStateOf<PackState?>(null)
|
||||||
private val fOwnRequests = mutableStateListOf<ReqState>().apply {
|
private val mOwnRequests = mutableStateListOf<ReqState>().apply {
|
||||||
inPack.requests.forEach { add(ReqState(it)) } // packs (and the loose root) may be empty
|
inPack.requests.forEach { add(ReqState(it)) } // packs (and the loose root) may be empty
|
||||||
}
|
}
|
||||||
val requests: androidx.compose.runtime.snapshots.SnapshotStateList<ReqState>
|
val requests: androidx.compose.runtime.snapshots.SnapshotStateList<ReqState>
|
||||||
get() = linkedSource?.requests ?: fOwnRequests
|
get() = linkedSource?.requests ?: mOwnRequests
|
||||||
val isLinked: Boolean get() = linkedSource != null
|
val isLinked: Boolean get() = linkedSource != null
|
||||||
var parent: PackState? =
|
var parent: PackState? =
|
||||||
null // enclosing pack (null = top-level / root)
|
null // enclosing pack (null = top-level / root)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import kotlinx.serialization.json.JsonElement
|
|||||||
// respects the editor's "tab size" (TextLayoutConfig.tabWidth), exactly like
|
// respects the editor's "tab size" (TextLayoutConfig.tabWidth), exactly like
|
||||||
// typed tabs. Lenient so slightly-off JSON (single quotes, trailing commas)
|
// typed tabs. Lenient so slightly-off JSON (single quotes, trailing commas)
|
||||||
// still reformats instead of failing.
|
// still reformats instead of failing.
|
||||||
private val fPrettyJson = Json {
|
private val mPrettyJson = Json {
|
||||||
prettyPrint = true
|
prettyPrint = true
|
||||||
prettyPrintIndent = "\t"
|
prettyPrintIndent = "\t"
|
||||||
isLenient = true
|
isLenient = true
|
||||||
@@ -23,7 +23,7 @@ private val fPrettyJson = Json {
|
|||||||
fun formatBody(inText: String, inFormat: BodyFormat): String =
|
fun formatBody(inText: String, inFormat: BodyFormat): String =
|
||||||
when (inFormat) {
|
when (inFormat) {
|
||||||
BodyFormat.JSON -> runCatching {
|
BodyFormat.JSON -> runCatching {
|
||||||
fPrettyJson.encodeToString(JsonElement.serializer(), fPrettyJson.parseToJsonElement(inText))
|
mPrettyJson.encodeToString(JsonElement.serializer(), mPrettyJson.parseToJsonElement(inText))
|
||||||
}.getOrDefault(inText)
|
}.getOrDefault(inText)
|
||||||
|
|
||||||
BodyFormat.XML -> runCatching { formatXml(inText) }.getOrDefault(inText)
|
BodyFormat.XML -> runCatching { formatXml(inText) }.getOrDefault(inText)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ macOS/Linux). Same TLS stack as the client-cert path in CurlMtls.kt.
|
|||||||
run() is a suspend fun - call it off the UI dispatcher. */
|
run() is a suspend fun - call it off the UI dispatcher. */
|
||||||
class HttpRunner {
|
class HttpRunner {
|
||||||
|
|
||||||
private val fClient = createApiHttpClient()
|
private val mClient = createApiHttpClient()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// Clear any temporary client certs a prior crash left in the Windows
|
// Clear any temporary client certs a prior crash left in the Windows
|
||||||
@@ -36,7 +36,7 @@ class HttpRunner {
|
|||||||
if (inReq.hasClientCert) return curlSendWithClientCert(inReq)
|
if (inReq.hasClientCert) return curlSendWithClientCert(inReq)
|
||||||
val vMark = TimeSource.Monotonic.markNow()
|
val vMark = TimeSource.Monotonic.markNow()
|
||||||
return try {
|
return try {
|
||||||
val vResp: HttpResponse = fClient.request(inReq.url.trim()) {
|
val vResp: HttpResponse = mClient.request(inReq.url.trim()) {
|
||||||
method = HttpMethod.parse(inReq.method.name)
|
method = HttpMethod.parse(inReq.method.name)
|
||||||
inReq.params
|
inReq.params
|
||||||
.filter { it.enabled && it.key.isNotBlank() }
|
.filter { it.enabled && it.key.isNotBlank() }
|
||||||
@@ -113,7 +113,7 @@ class HttpRunner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun close() = fClient.close()
|
fun close() = mClient.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Heuristic for a non-text body: textual content types (text, json, xml, html,
|
/** Heuristic for a non-text body: textual content types (text, json, xml, html,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import okio.Path.Companion.toPath
|
|||||||
// MARK: Pack persistence (okio + kotlinx.serialization)
|
// MARK: Pack persistence (okio + kotlinx.serialization)
|
||||||
// ==================
|
// ==================
|
||||||
|
|
||||||
private val fJson = Json {
|
private val mJson = Json {
|
||||||
prettyPrint = true
|
prettyPrint = true
|
||||||
prettyPrintIndent = " "
|
prettyPrintIndent = " "
|
||||||
ignoreUnknownKeys = true
|
ignoreUnknownKeys = true
|
||||||
@@ -20,7 +20,7 @@ private val fJson = Json {
|
|||||||
error message. */
|
error message. */
|
||||||
fun exportPack(inPack: Pack, inPath: String): String? = try {
|
fun exportPack(inPack: Pack, inPath: String): String? = try {
|
||||||
systemFileSystem.write(inPath.trim().toPath()) {
|
systemFileSystem.write(inPath.trim().toPath()) {
|
||||||
writeUtf8(fJson.encodeToString(inPack))
|
writeUtf8(mJson.encodeToString(inPack))
|
||||||
}
|
}
|
||||||
null
|
null
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
@@ -30,7 +30,7 @@ fun exportPack(inPack: Pack, inPath: String): String? = try {
|
|||||||
/** Read a pack back from inPath. */
|
/** Read a pack back from inPath. */
|
||||||
fun importPack(inPath: String): Result<Pack> = try {
|
fun importPack(inPath: String): Result<Pack> = try {
|
||||||
val vText = systemFileSystem.read(inPath.trim().toPath()) { readUtf8() }
|
val vText = systemFileSystem.read(inPath.trim().toPath()) { readUtf8() }
|
||||||
Result.success(fJson.decodeFromString<Pack>(vText))
|
Result.success(mJson.decodeFromString<Pack>(vText))
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Result.failure(e)
|
Result.failure(e)
|
||||||
}
|
}
|
||||||
@@ -38,7 +38,7 @@ fun importPack(inPath: String): Result<Pack> = try {
|
|||||||
/** Write a whole self-contained session to inPath. Returns null on success. */
|
/** Write a whole self-contained session to inPath. Returns null on success. */
|
||||||
fun exportSession(inSession: Session, inPath: String): String? = try {
|
fun exportSession(inSession: Session, inPath: String): String? = try {
|
||||||
systemFileSystem.write(inPath.trim().toPath()) {
|
systemFileSystem.write(inPath.trim().toPath()) {
|
||||||
writeUtf8(fJson.encodeToString(inSession))
|
writeUtf8(mJson.encodeToString(inSession))
|
||||||
}
|
}
|
||||||
null
|
null
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
@@ -48,7 +48,7 @@ fun exportSession(inSession: Session, inPath: String): String? = try {
|
|||||||
/** Read a session back from inPath. */
|
/** Read a session back from inPath. */
|
||||||
fun importSession(inPath: String): Result<Session> = try {
|
fun importSession(inPath: String): Result<Session> = try {
|
||||||
val vText = systemFileSystem.read(inPath.trim().toPath()) { readUtf8() }
|
val vText = systemFileSystem.read(inPath.trim().toPath()) { readUtf8() }
|
||||||
Result.success(fJson.decodeFromString<Session>(vText))
|
Result.success(mJson.decodeFromString<Session>(vText))
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Result.failure(e)
|
Result.failure(e)
|
||||||
}
|
}
|
||||||
@@ -85,7 +85,7 @@ fun writeBytesFile(inPath: String, inBytes: ByteArray): String? = try {
|
|||||||
/** Re-indent a JSON response for display; returns the input unchanged if it
|
/** Re-indent a JSON response for display; returns the input unchanged if it
|
||||||
isn't valid JSON (so plain-text / HTML responses still show). */
|
isn't valid JSON (so plain-text / HTML responses still show). */
|
||||||
fun prettyJsonOrRaw(inText: String): String = try {
|
fun prettyJsonOrRaw(inText: String): String = try {
|
||||||
fJson.encodeToString(JsonElement.serializer(), fJson.parseToJsonElement(inText))
|
mJson.encodeToString(JsonElement.serializer(), mJson.parseToJsonElement(inText))
|
||||||
} catch (_: Throwable) {
|
} catch (_: Throwable) {
|
||||||
inText
|
inText
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ data class Session(
|
|||||||
// MARK: Load / save (app-data dir via okio)
|
// MARK: Load / save (app-data dir via okio)
|
||||||
// ==================
|
// ==================
|
||||||
|
|
||||||
private val fStateJson = Json {
|
private val mStateJson = Json {
|
||||||
ignoreUnknownKeys = true
|
ignoreUnknownKeys = true
|
||||||
encodeDefaults = true
|
encodeDefaults = true
|
||||||
prettyPrint = true
|
prettyPrint = true
|
||||||
@@ -88,7 +88,7 @@ fun loadAppState(): AppState {
|
|||||||
val vPath = stateFilePath() ?: return AppState()
|
val vPath = stateFilePath() ?: return AppState()
|
||||||
return try {
|
return try {
|
||||||
val vText = systemFileSystem.read(vPath.toPath()) { readUtf8() }
|
val vText = systemFileSystem.read(vPath.toPath()) { readUtf8() }
|
||||||
fStateJson.decodeFromString<AppState>(vText)
|
mStateJson.decodeFromString<AppState>(vText)
|
||||||
} catch (_: Throwable) {
|
} catch (_: Throwable) {
|
||||||
AppState()
|
AppState()
|
||||||
}
|
}
|
||||||
@@ -99,7 +99,7 @@ the session, not the user's exported .json packs). */
|
|||||||
fun saveAppState(inState: AppState) {
|
fun saveAppState(inState: AppState) {
|
||||||
val vPath = stateFilePath() ?: return
|
val vPath = stateFilePath() ?: return
|
||||||
try {
|
try {
|
||||||
systemFileSystem.write(vPath.toPath()) { writeUtf8(fStateJson.encodeToString(inState)) }
|
systemFileSystem.write(vPath.toPath()) { writeUtf8(mStateJson.encodeToString(inState)) }
|
||||||
} catch (_: Throwable) {
|
} catch (_: Throwable) {
|
||||||
// ignore - nothing the user can act on, and packs can still be exported
|
// ignore - nothing the user can act on, and packs can still be exported
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -157,9 +157,9 @@ actual fun wrappedRowCount(inText: String, inFontPx: Int, inMaxWidthPx: Int, inF
|
|||||||
|
|
||||||
/** The jvm parity app only stores the preference - upstream's text pipeline has
|
/** The jvm parity app only stores the preference - upstream's text pipeline has
|
||||||
no global tab-width knob. Backed by state so menu checkmarks recompose. */
|
no global tab-width knob. Backed by state so menu checkmarks recompose. */
|
||||||
private var fTabWidth by mutableStateOf(4)
|
private var mTabWidth by mutableStateOf(4)
|
||||||
actual var editorTabWidth: Int
|
actual var editorTabWidth: Int
|
||||||
get() = fTabWidth
|
get() = mTabWidth
|
||||||
set(value) {
|
set(value) {
|
||||||
fTabWidth = value
|
mTabWidth = value
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,12 +11,6 @@ plugins {
|
|||||||
alias(libs.plugins.kotlin.plugin.compose)
|
alias(libs.plugins.kotlin.plugin.compose)
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
|
|
||||||
}
|
|
||||||
|
|
||||||
val vHostSupportsMingw = rootProject.extra["vHostSupportsMingw"] as Boolean
|
val vHostSupportsMingw = rootProject.extra["vHostSupportsMingw"] as Boolean
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
|
|||||||
+11
-11
@@ -51,20 +51,20 @@ private class SvgPainter(private val bytes: ByteArray) : Painter() {
|
|||||||
|
|
||||||
override val intrinsicSize: Size = svgIntrinsicSize(bytes) ?: Size.Unspecified
|
override val intrinsicSize: Size = svgIntrinsicSize(bytes) ?: Size.Unspecified
|
||||||
|
|
||||||
private var fAlpha: Float = 1f
|
private var mAlpha: Float = 1f
|
||||||
private var fColorFilter: ColorFilter? = null
|
private var mColorFilter: ColorFilter? = null
|
||||||
private var fCacheKey: Long = -1L
|
private var mCacheKey: Long = -1L
|
||||||
private var fCacheBitmap: ImageBitmap? = null
|
private var mCacheBitmap: ImageBitmap? = null
|
||||||
|
|
||||||
override fun applyAlpha(alpha: Float): Boolean { fAlpha = alpha; return true }
|
override fun applyAlpha(alpha: Float): Boolean { mAlpha = alpha; return true }
|
||||||
override fun applyColorFilter(colorFilter: ColorFilter?): Boolean { fColorFilter = colorFilter; return true }
|
override fun applyColorFilter(colorFilter: ColorFilter?): Boolean { mColorFilter = colorFilter; return true }
|
||||||
|
|
||||||
private fun rasterFor(inWidthPx: Int, inHeightPx: Int): ImageBitmap? {
|
private fun rasterFor(inWidthPx: Int, inHeightPx: Int): ImageBitmap? {
|
||||||
val vKey = (inWidthPx.toLong() shl 32) or inHeightPx.toLong()
|
val vKey = (inWidthPx.toLong() shl 32) or inHeightPx.toLong()
|
||||||
if (vKey == fCacheKey && fCacheBitmap != null) return fCacheBitmap
|
if (vKey == mCacheKey && mCacheBitmap != null) return mCacheBitmap
|
||||||
val vBitmap = decodeSvgAt(bytes, inWidthPx, inHeightPx) ?: return null
|
val vBitmap = decodeSvgAt(bytes, inWidthPx, inHeightPx) ?: return null
|
||||||
fCacheKey = vKey
|
mCacheKey = vKey
|
||||||
fCacheBitmap = vBitmap
|
mCacheBitmap = vBitmap
|
||||||
return vBitmap
|
return vBitmap
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,8 +79,8 @@ private class SvgPainter(private val bytes: ByteArray) : Painter() {
|
|||||||
srcSize = IntSize(vBitmap.width, vBitmap.height),
|
srcSize = IntSize(vBitmap.width, vBitmap.height),
|
||||||
dstOffset = IntOffset.Zero,
|
dstOffset = IntOffset.Zero,
|
||||||
dstSize = IntSize(vWpx, vHpx),
|
dstSize = IntSize(vWpx, vHpx),
|
||||||
alpha = fAlpha,
|
alpha = mAlpha,
|
||||||
colorFilter = fColorFilter,
|
colorFilter = mColorFilter,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+46
-46
@@ -32,13 +32,13 @@ private class Attr(val prefix: String, val localName: String, val nsUri: String,
|
|||||||
val qualifiedName: String get() = if (prefix.isEmpty()) localName else "$prefix:$localName"
|
val qualifiedName: String get() = if (prefix.isEmpty()) localName else "$prefix:$localName"
|
||||||
}
|
}
|
||||||
|
|
||||||
private class NodeListImpl(private val fNodes: List<Node>) : NodeList {
|
private class NodeListImpl(private val mNodes: List<Node>) : NodeList {
|
||||||
override fun item(i: Int): Node = fNodes[i]
|
override fun item(i: Int): Node = mNodes[i]
|
||||||
override val length: Int get() = fNodes.size
|
override val length: Int get() = mNodes.size
|
||||||
}
|
}
|
||||||
|
|
||||||
private class TextNodeImpl(private val fText: String) : Node {
|
private class TextNodeImpl(private val mText: String) : Node {
|
||||||
override val textContent: String? get() = fText
|
override val textContent: String? get() = mText
|
||||||
override val nodeName: String get() = "#text"
|
override val nodeName: String get() = "#text"
|
||||||
override val localName: String get() = ""
|
override val localName: String get() = ""
|
||||||
override val childNodes: NodeList = NodeListImpl(emptyList())
|
override val childNodes: NodeList = NodeListImpl(emptyList())
|
||||||
@@ -47,16 +47,16 @@ private class TextNodeImpl(private val fText: String) : Node {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private class ElementImpl(
|
private class ElementImpl(
|
||||||
private val fPrefix: String,
|
private val mPrefix: String,
|
||||||
override val localName: String,
|
override val localName: String,
|
||||||
override val namespaceURI: String,
|
override val namespaceURI: String,
|
||||||
private val fAttrs: List<Attr>,
|
private val mAttrs: List<Attr>,
|
||||||
// Innermost-first chain of xmlns scopes: each is prefix -> uri ("" = default ns).
|
// Innermost-first chain of xmlns scopes: each is prefix -> uri ("" = default ns).
|
||||||
private val fNsScope: List<Map<String, String>>,
|
private val mNsScope: List<Map<String, String>>,
|
||||||
) : Element {
|
) : Element {
|
||||||
var children: List<Node> = emptyList()
|
var children: List<Node> = emptyList()
|
||||||
|
|
||||||
override val nodeName: String get() = if (fPrefix.isEmpty()) localName else "$fPrefix:$localName"
|
override val nodeName: String get() = if (mPrefix.isEmpty()) localName else "$mPrefix:$localName"
|
||||||
override val childNodes: NodeList get() = NodeListImpl(children)
|
override val childNodes: NodeList get() = NodeListImpl(children)
|
||||||
|
|
||||||
// Concatenated descendant text - what DOM's textContent does.
|
// Concatenated descendant text - what DOM's textContent does.
|
||||||
@@ -72,7 +72,7 @@ private class ElementImpl(
|
|||||||
|
|
||||||
override fun lookupPrefix(namespaceURI: String): String {
|
override fun lookupPrefix(namespaceURI: String): String {
|
||||||
if (namespaceURI.isEmpty()) return ""
|
if (namespaceURI.isEmpty()) return ""
|
||||||
for (vScope in fNsScope) {
|
for (vScope in mNsScope) {
|
||||||
for ((vPrefix, vUri) in vScope) {
|
for ((vPrefix, vUri) in vScope) {
|
||||||
if (vUri == namespaceURI && vPrefix.isNotEmpty()) return vPrefix
|
if (vUri == namespaceURI && vPrefix.isNotEmpty()) return vPrefix
|
||||||
}
|
}
|
||||||
@@ -81,22 +81,22 @@ private class ElementImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getAttributeNS(nameSpaceURI: String, localName: String): String =
|
override fun getAttributeNS(nameSpaceURI: String, localName: String): String =
|
||||||
fAttrs.firstOrNull { it.nsUri == nameSpaceURI && it.localName == localName }?.value ?: ""
|
mAttrs.firstOrNull { it.nsUri == nameSpaceURI && it.localName == localName }?.value ?: ""
|
||||||
|
|
||||||
override fun getAttribute(name: String): String =
|
override fun getAttribute(name: String): String =
|
||||||
fAttrs.firstOrNull { it.qualifiedName == name }?.value ?: ""
|
mAttrs.firstOrNull { it.qualifiedName == name }?.value ?: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============
|
// ============
|
||||||
// The parser - one pass over the string with a cursor.
|
// The parser - one pass over the string with a cursor.
|
||||||
private class XmlDomParser(private val fXml: String) {
|
private class XmlDomParser(private val mXml: String) {
|
||||||
private var fPos = 0
|
private var mPos = 0
|
||||||
|
|
||||||
fun skipProlog() {
|
fun skipProlog() {
|
||||||
while (true) {
|
while (true) {
|
||||||
skipWhitespace()
|
skipWhitespace()
|
||||||
when {
|
when {
|
||||||
lookingAt("") -> fPos++
|
lookingAt("") -> mPos++
|
||||||
lookingAt("<?") -> skipUntil("?>")
|
lookingAt("<?") -> skipUntil("?>")
|
||||||
lookingAt("<!--") -> skipUntil("-->")
|
lookingAt("<!--") -> skipUntil("-->")
|
||||||
lookingAt("<!DOCTYPE") -> skipDoctype()
|
lookingAt("<!DOCTYPE") -> skipDoctype()
|
||||||
@@ -112,7 +112,7 @@ private class XmlDomParser(private val fXml: String) {
|
|||||||
if (!lookingAt("<") || lookingAt("</")) return null
|
if (!lookingAt("<") || lookingAt("</")) return null
|
||||||
expect("<")
|
expect("<")
|
||||||
val vQName = readName()
|
val vQName = readName()
|
||||||
if (vQName.isEmpty()) throw MalformedXMLException("empty element name at $fPos")
|
if (vQName.isEmpty()) throw MalformedXMLException("empty element name at $mPos")
|
||||||
|
|
||||||
// Attributes (collect raw first - xmlns declarations shape the scope
|
// Attributes (collect raw first - xmlns declarations shape the scope
|
||||||
// that THIS element's own prefix resolves against).
|
// that THIS element's own prefix resolves against).
|
||||||
@@ -121,7 +121,7 @@ private class XmlDomParser(private val fXml: String) {
|
|||||||
skipWhitespace()
|
skipWhitespace()
|
||||||
if (lookingAt("/>") || lookingAt(">")) break
|
if (lookingAt("/>") || lookingAt(">")) break
|
||||||
val vAttrName = readName()
|
val vAttrName = readName()
|
||||||
if (vAttrName.isEmpty()) throw MalformedXMLException("bad attribute at $fPos")
|
if (vAttrName.isEmpty()) throw MalformedXMLException("bad attribute at $mPos")
|
||||||
skipWhitespace(); expect("="); skipWhitespace()
|
skipWhitespace(); expect("="); skipWhitespace()
|
||||||
vRawAttrs.add(vAttrName to readQuotedValue())
|
vRawAttrs.add(vAttrName to readQuotedValue())
|
||||||
}
|
}
|
||||||
@@ -160,7 +160,7 @@ private class XmlDomParser(private val fXml: String) {
|
|||||||
val vElement = ElementImpl(vPrefix, vLocal, resolveNs(vPrefix), vAttrs, vScope)
|
val vElement = ElementImpl(vPrefix, vLocal, resolveNs(vPrefix), vAttrs, vScope)
|
||||||
|
|
||||||
if (lookingAt("/>")) {
|
if (lookingAt("/>")) {
|
||||||
fPos += 2
|
mPos += 2
|
||||||
return vElement
|
return vElement
|
||||||
}
|
}
|
||||||
expect(">")
|
expect(">")
|
||||||
@@ -169,9 +169,9 @@ private class XmlDomParser(private val fXml: String) {
|
|||||||
val vChildren = ArrayList<Node>()
|
val vChildren = ArrayList<Node>()
|
||||||
while (true) {
|
while (true) {
|
||||||
when {
|
when {
|
||||||
fPos >= fXml.length -> throw MalformedXMLException("unclosed <$vQName>")
|
mPos >= mXml.length -> throw MalformedXMLException("unclosed <$vQName>")
|
||||||
lookingAt("</") -> {
|
lookingAt("</") -> {
|
||||||
fPos += 2
|
mPos += 2
|
||||||
val vClose = readName()
|
val vClose = readName()
|
||||||
skipWhitespace(); expect(">")
|
skipWhitespace(); expect(">")
|
||||||
if (vClose != vQName) throw MalformedXMLException("mismatched </$vClose> for <$vQName>")
|
if (vClose != vQName) throw MalformedXMLException("mismatched </$vClose> for <$vQName>")
|
||||||
@@ -180,18 +180,18 @@ private class XmlDomParser(private val fXml: String) {
|
|||||||
}
|
}
|
||||||
lookingAt("<!--") -> skipUntil("-->")
|
lookingAt("<!--") -> skipUntil("-->")
|
||||||
lookingAt("<![CDATA[") -> {
|
lookingAt("<![CDATA[") -> {
|
||||||
val vEnd = fXml.indexOf("]]>", fPos + 9)
|
val vEnd = mXml.indexOf("]]>", mPos + 9)
|
||||||
if (vEnd < 0) throw MalformedXMLException("unclosed CDATA")
|
if (vEnd < 0) throw MalformedXMLException("unclosed CDATA")
|
||||||
vChildren.add(TextNodeImpl(fXml.substring(fPos + 9, vEnd)))
|
vChildren.add(TextNodeImpl(mXml.substring(mPos + 9, vEnd)))
|
||||||
fPos = vEnd + 3
|
mPos = vEnd + 3
|
||||||
}
|
}
|
||||||
lookingAt("<?") -> skipUntil("?>")
|
lookingAt("<?") -> skipUntil("?>")
|
||||||
lookingAt("<") -> parseElement(vScope)?.let { vChildren.add(it) }
|
lookingAt("<") -> parseElement(vScope)?.let { vChildren.add(it) }
|
||||||
else -> {
|
else -> {
|
||||||
val vNext = fXml.indexOf('<', fPos).let { if (it < 0) fXml.length else it }
|
val vNext = mXml.indexOf('<', mPos).let { if (it < 0) mXml.length else it }
|
||||||
val vText = decodeEntities(fXml.substring(fPos, vNext))
|
val vText = decodeEntities(mXml.substring(mPos, vNext))
|
||||||
if (vText.isNotBlank()) vChildren.add(TextNodeImpl(vText))
|
if (vText.isNotBlank()) vChildren.add(TextNodeImpl(vText))
|
||||||
fPos = vNext
|
mPos = vNext
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -200,28 +200,28 @@ private class XmlDomParser(private val fXml: String) {
|
|||||||
// ============
|
// ============
|
||||||
// Lexing helpers
|
// Lexing helpers
|
||||||
|
|
||||||
private fun lookingAt(inToken: String): Boolean = fXml.startsWith(inToken, fPos)
|
private fun lookingAt(inToken: String): Boolean = mXml.startsWith(inToken, mPos)
|
||||||
|
|
||||||
private fun expect(inToken: String) {
|
private fun expect(inToken: String) {
|
||||||
if (!lookingAt(inToken)) throw MalformedXMLException("expected '$inToken' at $fPos")
|
if (!lookingAt(inToken)) throw MalformedXMLException("expected '$inToken' at $mPos")
|
||||||
fPos += inToken.length
|
mPos += inToken.length
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun skipWhitespace() {
|
private fun skipWhitespace() {
|
||||||
while (fPos < fXml.length && fXml[fPos].isWhitespace()) fPos++
|
while (mPos < mXml.length && mXml[mPos].isWhitespace()) mPos++
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun skipUntil(inToken: String) {
|
private fun skipUntil(inToken: String) {
|
||||||
val vEnd = fXml.indexOf(inToken, fPos)
|
val vEnd = mXml.indexOf(inToken, mPos)
|
||||||
if (vEnd < 0) throw MalformedXMLException("unterminated '$inToken' section")
|
if (vEnd < 0) throw MalformedXMLException("unterminated '$inToken' section")
|
||||||
fPos = vEnd + inToken.length
|
mPos = vEnd + inToken.length
|
||||||
}
|
}
|
||||||
|
|
||||||
/* DOCTYPE may nest an internal subset in [ ] - skip to the matching '>'. */
|
/* DOCTYPE may nest an internal subset in [ ] - skip to the matching '>'. */
|
||||||
private fun skipDoctype() {
|
private fun skipDoctype() {
|
||||||
var vDepth = 0
|
var vDepth = 0
|
||||||
while (fPos < fXml.length) {
|
while (mPos < mXml.length) {
|
||||||
val c = fXml[fPos++]
|
val c = mXml[mPos++]
|
||||||
if (c == '[') vDepth++
|
if (c == '[') vDepth++
|
||||||
else if (c == ']') vDepth--
|
else if (c == ']') vDepth--
|
||||||
else if (c == '>' && vDepth <= 0) return
|
else if (c == '>' && vDepth <= 0) return
|
||||||
@@ -230,24 +230,24 @@ private class XmlDomParser(private val fXml: String) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun readName(): String {
|
private fun readName(): String {
|
||||||
val vStart = fPos
|
val vStart = mPos
|
||||||
while (fPos < fXml.length) {
|
while (mPos < mXml.length) {
|
||||||
val c = fXml[fPos]
|
val c = mXml[mPos]
|
||||||
if (c.isWhitespace() || c == '=' || c == '>' || c == '/' || c == '<') break
|
if (c.isWhitespace() || c == '=' || c == '>' || c == '/' || c == '<') break
|
||||||
fPos++
|
mPos++
|
||||||
}
|
}
|
||||||
return fXml.substring(vStart, fPos)
|
return mXml.substring(vStart, mPos)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun readQuotedValue(): String {
|
private fun readQuotedValue(): String {
|
||||||
if (fPos >= fXml.length) throw MalformedXMLException("unterminated attribute value")
|
if (mPos >= mXml.length) throw MalformedXMLException("unterminated attribute value")
|
||||||
val vQuote = fXml[fPos]
|
val vQuote = mXml[mPos]
|
||||||
if (vQuote != '"' && vQuote != '\'') throw MalformedXMLException("attribute value must be quoted at $fPos")
|
if (vQuote != '"' && vQuote != '\'') throw MalformedXMLException("attribute value must be quoted at $mPos")
|
||||||
fPos++
|
mPos++
|
||||||
val vEnd = fXml.indexOf(vQuote, fPos)
|
val vEnd = mXml.indexOf(vQuote, mPos)
|
||||||
if (vEnd < 0) throw MalformedXMLException("unterminated attribute value")
|
if (vEnd < 0) throw MalformedXMLException("unterminated attribute value")
|
||||||
val vRaw = fXml.substring(fPos, vEnd)
|
val vRaw = mXml.substring(mPos, vEnd)
|
||||||
fPos = vEnd + 1
|
mPos = vEnd + 1
|
||||||
return decodeEntities(vRaw)
|
return decodeEntities(vRaw)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ final var com.compose.sdl/useVirtualFrameTime // com.compose.sdl/useVirtualFrame
|
|||||||
final fun <get-useVirtualFrameTime>(): kotlin/Boolean // com.compose.sdl/useVirtualFrameTime.<get-useVirtualFrameTime>|<get-useVirtualFrameTime>(){}[0]
|
final fun <get-useVirtualFrameTime>(): kotlin/Boolean // com.compose.sdl/useVirtualFrameTime.<get-useVirtualFrameTime>|<get-useVirtualFrameTime>(){}[0]
|
||||||
final fun <set-useVirtualFrameTime>(kotlin/Boolean) // com.compose.sdl/useVirtualFrameTime.<set-useVirtualFrameTime>|<set-useVirtualFrameTime>(kotlin.Boolean){}[0]
|
final fun <set-useVirtualFrameTime>(kotlin/Boolean) // com.compose.sdl/useVirtualFrameTime.<set-useVirtualFrameTime>|<set-useVirtualFrameTime>(kotlin.Boolean){}[0]
|
||||||
|
|
||||||
final fun (com.compose.sdl/ApplicationScope).com.compose.sdl/Window(kotlin/Function0<kotlin/Unit>, kotlin/String?, kotlin/Int, kotlin/Int, com.compose.sdl/GpuMode?, com.compose.sdl/AppWindowIcon?, kotlin/Function2<com.compose.sdl/RenderBackend, kotlin/Int, kotlin/Boolean>?, kotlin/Function3<com.compose.sdl/ComposeWindowScope, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.compose.sdl/Window|Window@com.compose.sdl.ApplicationScope(kotlin.Function0<kotlin.Unit>;kotlin.String?;kotlin.Int;kotlin.Int;com.compose.sdl.GpuMode?;com.compose.sdl.AppWindowIcon?;kotlin.Function2<com.compose.sdl.RenderBackend,kotlin.Int,kotlin.Boolean>?;kotlin.Function3<com.compose.sdl.ComposeWindowScope,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
final fun (com.compose.sdl/ApplicationScope).com.compose.sdl/Window(kotlin/Function0<kotlin/Unit>, kotlin/String?, kotlin/Int, kotlin/Int, com.compose.sdl/GpuMode?, com.compose.sdl/AppWindowIcon?, kotlin/Boolean, kotlin/Boolean, kotlin/Boolean, kotlin/Boolean, kotlin/Boolean, kotlin/Boolean, kotlin/Function1<androidx.compose.ui.input.key/KeyEvent, kotlin/Boolean>?, kotlin/Function1<androidx.compose.ui.input.key/KeyEvent, kotlin/Boolean>?, kotlin/Function2<com.compose.sdl/RenderBackend, kotlin/Int, kotlin/Boolean>?, kotlin/Function3<com.compose.sdl/ComposeWindowScope, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // com.compose.sdl/Window|Window@com.compose.sdl.ApplicationScope(kotlin.Function0<kotlin.Unit>;kotlin.String?;kotlin.Int;kotlin.Int;com.compose.sdl.GpuMode?;com.compose.sdl.AppWindowIcon?;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;kotlin.Function1<androidx.compose.ui.input.key.KeyEvent,kotlin.Boolean>?;kotlin.Function1<androidx.compose.ui.input.key.KeyEvent,kotlin.Boolean>?;kotlin.Function2<com.compose.sdl.RenderBackend,kotlin.Int,kotlin.Boolean>?;kotlin.Function3<com.compose.sdl.ComposeWindowScope,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){}[0]
|
||||||
final fun com.compose.sdl/com_compose_sdl_AppWindowIcon$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_AppWindowIcon$stableprop_getter|com_compose_sdl_AppWindowIcon$stableprop_getter(){}[0]
|
final fun com.compose.sdl/com_compose_sdl_AppWindowIcon$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_AppWindowIcon$stableprop_getter|com_compose_sdl_AppWindowIcon$stableprop_getter(){}[0]
|
||||||
final fun com.compose.sdl/nativeComposeApp(kotlin/Function3<com.compose.sdl/ApplicationScope, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>) // com.compose.sdl/nativeComposeApp|nativeComposeApp(kotlin.Function3<com.compose.sdl.ApplicationScope,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>){}[0]
|
final fun com.compose.sdl/nativeComposeApp(kotlin/Function3<com.compose.sdl/ApplicationScope, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>) // com.compose.sdl/nativeComposeApp|nativeComposeApp(kotlin.Function3<com.compose.sdl.ApplicationScope,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>){}[0]
|
||||||
final fun com.compose.sdl/nativeComposeWindow(kotlin/String = ..., kotlin/Int = ..., kotlin/Int = ..., com.compose.sdl/GpuMode = ..., com.compose.sdl/AppWindowIcon? = ..., kotlin/Function2<com.compose.sdl/RenderBackend, kotlin/Int, kotlin/Boolean>? = ..., kotlin/Function3<com.compose.sdl/ComposeWindowScope, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>) // com.compose.sdl/nativeComposeWindow|nativeComposeWindow(kotlin.String;kotlin.Int;kotlin.Int;com.compose.sdl.GpuMode;com.compose.sdl.AppWindowIcon?;kotlin.Function2<com.compose.sdl.RenderBackend,kotlin.Int,kotlin.Boolean>?;kotlin.Function3<com.compose.sdl.ComposeWindowScope,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>){}[0]
|
final fun com.compose.sdl/nativeComposeWindow(kotlin/String = ..., kotlin/Int = ..., kotlin/Int = ..., com.compose.sdl/GpuMode = ..., com.compose.sdl/AppWindowIcon? = ..., kotlin/Boolean = ..., kotlin/Boolean = ..., kotlin/Boolean = ..., kotlin/Boolean = ..., kotlin/Boolean = ..., kotlin/Boolean = ..., kotlin/Function1<androidx.compose.ui.input.key/KeyEvent, kotlin/Boolean> = ..., kotlin/Function1<androidx.compose.ui.input.key/KeyEvent, kotlin/Boolean> = ..., kotlin/Function2<com.compose.sdl/RenderBackend, kotlin/Int, kotlin/Boolean>? = ..., kotlin/Function3<com.compose.sdl/ComposeWindowScope, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>) // com.compose.sdl/nativeComposeWindow|nativeComposeWindow(kotlin.String;kotlin.Int;kotlin.Int;com.compose.sdl.GpuMode;com.compose.sdl.AppWindowIcon?;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;kotlin.Function1<androidx.compose.ui.input.key.KeyEvent,kotlin.Boolean>;kotlin.Function1<androidx.compose.ui.input.key.KeyEvent,kotlin.Boolean>;kotlin.Function2<com.compose.sdl.RenderBackend,kotlin.Int,kotlin.Boolean>?;kotlin.Function3<com.compose.sdl.ComposeWindowScope,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>){}[0]
|
||||||
final fun com.compose.sdl/windowHasInvalidations(): kotlin/Boolean // com.compose.sdl/windowHasInvalidations|windowHasInvalidations(){}[0]
|
final fun com.compose.sdl/windowHasInvalidations(): kotlin/Boolean // com.compose.sdl/windowHasInvalidations|windowHasInvalidations(){}[0]
|
||||||
|
|||||||
+88
@@ -0,0 +1,88 @@
|
|||||||
|
@file:OptIn(
|
||||||
|
androidx.compose.ui.InternalComposeUiApi::class,
|
||||||
|
androidx.compose.runtime.InternalComposeApi::class,
|
||||||
|
)
|
||||||
|
|
||||||
|
package com.compose.sdl
|
||||||
|
|
||||||
|
import androidx.compose.runtime.AbstractApplier
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
||||||
|
// ==================
|
||||||
|
// MARK: App runtime internals
|
||||||
|
// ==================
|
||||||
|
|
||||||
|
internal class ApplicationScopeImpl(val runtime: AppRuntime) : ApplicationScope {
|
||||||
|
override fun exitApplication() { runtime.exitRequested = true }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Registry + lifecycle for the live windows. Windows are created by the app
|
||||||
|
composition (Window()'s remember) and destroyed by the LOOP - DisposableEffect
|
||||||
|
onDispose only schedules, because teardown disposes a composition and that
|
||||||
|
must not run re-entrantly inside another composition's apply pass. */
|
||||||
|
internal class AppRuntime {
|
||||||
|
lateinit var scope: CoroutineScope
|
||||||
|
val windows = mutableListOf<WindowInstance>()
|
||||||
|
private val destroyQueue = mutableListOf<WindowInstance>()
|
||||||
|
var exitRequested = false
|
||||||
|
var hadWindow = false
|
||||||
|
|
||||||
|
fun windowFor(inId: UInt): WindowInstance? =
|
||||||
|
if (inId == 0u) windows.firstOrNull()
|
||||||
|
else windows.firstOrNull { it.backend.windowId == inId } ?: windows.firstOrNull()
|
||||||
|
|
||||||
|
fun markAllNeedFrame() {
|
||||||
|
for (vW in windows) vW.needsFrame = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createWindow(
|
||||||
|
inTitle: String,
|
||||||
|
inWidth: Int,
|
||||||
|
inHeight: Int,
|
||||||
|
inGpu: GpuMode,
|
||||||
|
inIcon: AppWindowIcon?,
|
||||||
|
inAttrs: WindowAttributes,
|
||||||
|
inOnFrame: ((RenderBackend, Int) -> Boolean)?,
|
||||||
|
inPreviewKeyHandler: () -> ((androidx.compose.ui.input.key.KeyEvent) -> Boolean),
|
||||||
|
inKeyHandler: () -> ((androidx.compose.ui.input.key.KeyEvent) -> Boolean),
|
||||||
|
inContent: () -> (@Composable ComposeWindowScope.() -> Unit),
|
||||||
|
): WindowInstance {
|
||||||
|
val vWindow = WindowInstance(
|
||||||
|
inTitle, inWidth, inHeight, inGpu, inIcon, inAttrs, inOnFrame,
|
||||||
|
inPreviewKeyHandler, inKeyHandler, inContent,
|
||||||
|
)
|
||||||
|
// A Window() was declared either way - the "exit when the last window
|
||||||
|
// is gone" rule must also fire when every declared window failed to
|
||||||
|
// initialise (otherwise the loop would spin forever with none).
|
||||||
|
hadWindow = true
|
||||||
|
if (vWindow.init(scope)) {
|
||||||
|
windows.add(vWindow)
|
||||||
|
} else {
|
||||||
|
println("nativeComposeApp: window '$inTitle' failed to initialise")
|
||||||
|
}
|
||||||
|
return vWindow
|
||||||
|
}
|
||||||
|
|
||||||
|
fun scheduleDestroy(inWindow: WindowInstance) {
|
||||||
|
if (inWindow in windows && inWindow !in destroyQueue) destroyQueue.add(inWindow)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reapDestroyed() {
|
||||||
|
while (destroyQueue.isNotEmpty()) {
|
||||||
|
val vW = destroyQueue.removeLast()
|
||||||
|
windows.remove(vW)
|
||||||
|
vW.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No-op applier for the app-level composition - it emits no UI nodes, only
|
||||||
|
// side effects (each Window() manages a native window).
|
||||||
|
internal class UnitApplier : AbstractApplier<Unit>(Unit) {
|
||||||
|
override fun insertTopDown(index: Int, instance: Unit) {}
|
||||||
|
override fun insertBottomUp(index: Int, instance: Unit) {}
|
||||||
|
override fun remove(index: Int, count: Int) {}
|
||||||
|
override fun move(from: Int, to: Int, count: Int) {}
|
||||||
|
override fun onClear() {}
|
||||||
|
}
|
||||||
+47
-679
@@ -5,46 +5,21 @@
|
|||||||
|
|
||||||
package com.compose.sdl
|
package com.compose.sdl
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.runtime.AbstractApplier
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.input.key.KeyEvent
|
||||||
import androidx.compose.runtime.Composition
|
import androidx.compose.runtime.Composition
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
|
||||||
import androidx.compose.runtime.Recomposer
|
import androidx.compose.runtime.Recomposer
|
||||||
import androidx.compose.runtime.SideEffect
|
import androidx.compose.runtime.SideEffect
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberUpdatedState
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.runtime.snapshots.Snapshot
|
import androidx.compose.runtime.snapshots.Snapshot
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.input.key.key
|
|
||||||
import androidx.compose.ui.input.key.type
|
|
||||||
import androidx.compose.ui.input.pointer.PointerButton
|
|
||||||
import androidx.compose.ui.input.pointer.PointerEventType
|
|
||||||
import com.compose.sdl.node.ComposeRootHost
|
|
||||||
import com.compose.sdl.res.currentImageLoader
|
|
||||||
import com.compose.sdl.window.LocalPopupHost
|
|
||||||
import com.compose.sdl.window.PopupLayer
|
|
||||||
import com.compose.sdl.window.createPopupHostState
|
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.test.resetMain
|
import kotlinx.coroutines.test.resetMain
|
||||||
import kotlinx.coroutines.test.setMain
|
import kotlinx.coroutines.test.setMain
|
||||||
import kotlinx.cinterop.toKString
|
|
||||||
import kotlinx.cinterop.reinterpret
|
|
||||||
import kotlinx.cinterop.alloc
|
|
||||||
import kotlinx.cinterop.ptr
|
|
||||||
import kotlinx.cinterop.COpaquePointer
|
|
||||||
import kotlinx.cinterop.pointed
|
|
||||||
import sdl3.SDL_Delay
|
import sdl3.SDL_Delay
|
||||||
import sdl3.SDL_GetCurrentDisplayMode
|
|
||||||
import sdl3.SDL_GetDisplayForWindow
|
|
||||||
import sdl3.SDL_GetPerformanceCounter
|
|
||||||
import sdl3.SDL_GetPerformanceFrequency
|
|
||||||
import sdl3.SDL_GetTicks
|
import sdl3.SDL_GetTicks
|
||||||
import sdl3.SDL_GetTicksNS
|
|
||||||
import sdl3.SDL_Quit
|
import sdl3.SDL_Quit
|
||||||
import sdl3.SDL_SetWindowTitle
|
|
||||||
import sdl3.SDL_WaitEventTimeout
|
import sdl3.SDL_WaitEventTimeout
|
||||||
|
|
||||||
// ==================
|
// ==================
|
||||||
@@ -92,6 +67,14 @@ fun ApplicationScope.Window(
|
|||||||
height: Int = 600,
|
height: Int = 600,
|
||||||
gpu: GpuMode = GpuMode.Auto,
|
gpu: GpuMode = GpuMode.Auto,
|
||||||
icon: AppWindowIcon? = null,
|
icon: AppWindowIcon? = null,
|
||||||
|
undecorated: Boolean = false,
|
||||||
|
transparent: Boolean = false,
|
||||||
|
resizable: Boolean = true,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
focusable: Boolean = true,
|
||||||
|
alwaysOnTop: Boolean = false,
|
||||||
|
onPreviewKeyEvent: (KeyEvent) -> Boolean = { false },
|
||||||
|
onKeyEvent: (KeyEvent) -> Boolean = { false },
|
||||||
onFrame: ((backend: RenderBackend, frameIndex: Int) -> Boolean)? = null,
|
onFrame: ((backend: RenderBackend, frameIndex: Int) -> Boolean)? = null,
|
||||||
content: @Composable ComposeWindowScope.() -> Unit,
|
content: @Composable ComposeWindowScope.() -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -99,6 +82,16 @@ fun ApplicationScope.Window(
|
|||||||
?: error("Window() must be called inside nativeComposeApp { ... }")
|
?: error("Window() must be called inside nativeComposeApp { ... }")
|
||||||
val vContent = rememberUpdatedState(content)
|
val vContent = rememberUpdatedState(content)
|
||||||
val vClose = rememberUpdatedState(onCloseRequest)
|
val vClose = rememberUpdatedState(onCloseRequest)
|
||||||
|
val vPreviewKey = rememberUpdatedState(onPreviewKeyEvent)
|
||||||
|
val vKey = rememberUpdatedState(onKeyEvent)
|
||||||
|
val vAttrs = WindowAttributes(
|
||||||
|
undecorated = undecorated,
|
||||||
|
transparent = transparent,
|
||||||
|
resizable = resizable,
|
||||||
|
enabled = enabled,
|
||||||
|
focusable = focusable,
|
||||||
|
alwaysOnTop = alwaysOnTop,
|
||||||
|
)
|
||||||
val vWindow = remember {
|
val vWindow = remember {
|
||||||
vScope.runtime.createWindow(
|
vScope.runtime.createWindow(
|
||||||
inTitle = title,
|
inTitle = title,
|
||||||
@@ -106,7 +99,12 @@ fun ApplicationScope.Window(
|
|||||||
inHeight = height,
|
inHeight = height,
|
||||||
inGpu = gpu,
|
inGpu = gpu,
|
||||||
inIcon = icon,
|
inIcon = icon,
|
||||||
|
inAttrs = vAttrs,
|
||||||
inOnFrame = onFrame,
|
inOnFrame = onFrame,
|
||||||
|
// Read through holders so updated lambdas propagate without
|
||||||
|
// recreating the window (same reason as the content holder below).
|
||||||
|
inPreviewKeyHandler = { vPreviewKey.value },
|
||||||
|
inKeyHandler = { vKey.value },
|
||||||
// The window composition reads the holder each recomposition, so
|
// The window composition reads the holder each recomposition, so
|
||||||
// updated content lambdas propagate without recreating the window.
|
// updated content lambdas propagate without recreating the window.
|
||||||
inContent = { vContent.value },
|
inContent = { vContent.value },
|
||||||
@@ -115,6 +113,12 @@ fun ApplicationScope.Window(
|
|||||||
SideEffect {
|
SideEffect {
|
||||||
if (vWindow.facade.title != title) vWindow.facade.setTitle(title)
|
if (vWindow.facade.title != title) vWindow.facade.setTitle(title)
|
||||||
}
|
}
|
||||||
|
// Re-apply only on a real change; `transparent` is creation-only (see
|
||||||
|
// WindowAttributes) so it is deliberately not re-applied here.
|
||||||
|
DisposableEffect(vAttrs) {
|
||||||
|
vWindow.applyAttributes(vAttrs)
|
||||||
|
onDispose { }
|
||||||
|
}
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
onDispose { vScope.runtime.scheduleDestroy(vWindow) }
|
onDispose { vScope.runtime.scheduleDestroy(vWindow) }
|
||||||
}
|
}
|
||||||
@@ -136,7 +140,7 @@ fun nativeComposeApp(content: @Composable ApplicationScope.() -> Unit) {
|
|||||||
runtime.scope = this
|
runtime.scope = this
|
||||||
|
|
||||||
// App-level composition: no UI tree - it only declares Window()s.
|
// App-level composition: no UI tree - it only declares Window()s.
|
||||||
val appClock = SDL3FrameClock()
|
val appClock = Sdl3FrameClock()
|
||||||
val appRecomposer = Recomposer(coroutineContext + appClock)
|
val appRecomposer = Recomposer(coroutineContext + appClock)
|
||||||
// The clock must be in the collector's context - runRecomposeAndApplyChanges
|
// The clock must be in the collector's context - runRecomposeAndApplyChanges
|
||||||
// awaits parent frames through it.
|
// awaits parent frames through it.
|
||||||
@@ -311,74 +315,6 @@ fun nativeComposeApp(content: @Composable ApplicationScope.() -> Unit) {
|
|||||||
SDL_Quit()
|
SDL_Quit()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================
|
|
||||||
// MARK: Probe / screenshot support (render-to-quiescence)
|
|
||||||
// ==================
|
|
||||||
|
|
||||||
/** Set BEFORE nativeComposeApp/nativeComposeWindow: every window's composition then runs
|
|
||||||
under an InfiniteAnimationPolicy that CANCELS infinite animations, so
|
|
||||||
rememberInfiniteTransition & co. freeze at their initial value - the same mechanism
|
|
||||||
upstream's test rules use. Screenshot/parity runs enable it so looping screens can
|
|
||||||
reach quiescence and capture deterministically. */
|
|
||||||
var disableInfiniteAnimations: Boolean = false
|
|
||||||
|
|
||||||
/** The cancelling policy: a coroutine that ends in CancellationException counts as
|
|
||||||
cancelled (not failed), so only the animation coroutine stops - nothing propagates. */
|
|
||||||
private object CancelInfiniteAnimationsPolicy : androidx.compose.ui.platform.InfiniteAnimationPolicy {
|
|
||||||
override suspend fun <R> onInfiniteOperation(block: suspend () -> R): R =
|
|
||||||
throw CancellationException("infinite animations are disabled (disableInfiniteAnimations)")
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Set BEFORE nativeComposeApp/nativeComposeWindow: the composition + animation frame
|
|
||||||
clocks advance a VIRTUAL 16.6ms per main-loop iteration instead of reading SDL's
|
|
||||||
real-time ticks - the native mirror of the JVM parity leg's render(nanos) stepping.
|
|
||||||
Animations then progress by exact per-frame deltas, so anything time-raced (e.g. a
|
|
||||||
bring-into-view scroll interrupted mid-flight) resolves identically on every run and
|
|
||||||
screenshots become deterministic. Input timestamps and FPS stay on real time. */
|
|
||||||
var useVirtualFrameTime: Boolean = false
|
|
||||||
|
|
||||||
private var virtualFrameNanos = 0L
|
|
||||||
|
|
||||||
private fun advanceVirtualFrame() {
|
|
||||||
if (useVirtualFrameTime) virtualFrameNanos += 16_666_667L
|
|
||||||
}
|
|
||||||
|
|
||||||
// Timestamp for the composition frame clocks (recomposer + withFrameNanos animations).
|
|
||||||
private fun frameClockNanos(): Long =
|
|
||||||
if (useVirtualFrameTime) virtualFrameNanos else SDL_GetTicksNS().toLong()
|
|
||||||
|
|
||||||
// Timestamp for the owner's node-animation clock - real path keeps the pre-existing
|
|
||||||
// ms-resolution SDL_GetTicks base so non-screenshot behaviour is bit-for-bit unchanged.
|
|
||||||
private fun animationClockNanos(): Long =
|
|
||||||
if (useVirtualFrameTime) virtualFrameNanos else SDL_GetTicks().toLong() * 1_000_000L
|
|
||||||
|
|
||||||
// The window currently inside renderFrame - the loop is single-threaded, so a plain
|
|
||||||
// var is enough for onFrame probes to address "the window I'm being called for".
|
|
||||||
private var renderingWindow: WindowInstance? = null
|
|
||||||
|
|
||||||
/** From inside an onFrame callback: true while the just-rendered window still has pending
|
|
||||||
work (recomposition, layout/draw invalidation, or an animation awaiting the next frame).
|
|
||||||
Screenshot probes capture once this stays false for a few consecutive frames instead of
|
|
||||||
at a fixed frame count. Outside onFrame it answers false. */
|
|
||||||
fun windowHasInvalidations(): Boolean = renderingWindow?.hasInvalidations() ?: false
|
|
||||||
|
|
||||||
/** Trigger a Kotlin/Native GC so Cleaner-managed renderer resources release
|
|
||||||
their native memory (see the main loop's native-memory nudge). */
|
|
||||||
@OptIn(kotlin.native.runtime.NativeRuntimeApi::class)
|
|
||||||
private fun collectNativeGarbage() = kotlin.native.runtime.GC.collect()
|
|
||||||
|
|
||||||
/** Frame interval (ms) of the window's current display, for the non-vsync
|
|
||||||
fallback pacing (SDL_Delay). Falls back to 16ms (~60Hz) when the mode can't
|
|
||||||
be read, so a 144Hz panel on a non-vsync path isn't capped to 60. */
|
|
||||||
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
|
|
||||||
private fun displayFrameDelayMs(window: COpaquePointer?): UInt {
|
|
||||||
val vWindow = window ?: return 16u
|
|
||||||
val vDisplay = SDL_GetDisplayForWindow(vWindow.reinterpret())
|
|
||||||
val vMode = SDL_GetCurrentDisplayMode(vDisplay) ?: return 16u
|
|
||||||
val vHz = vMode.pointed.refresh_rate
|
|
||||||
return if (vHz > 0f) (1000f / vHz).toUInt().coerceAtLeast(1u) else 16u
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Single-window compatibility wrapper - the pre-multi-window entry point.
|
/** Single-window compatibility wrapper - the pre-multi-window entry point.
|
||||||
Closing the window exits the app, exactly as before. */
|
Closing the window exits the app, exactly as before. */
|
||||||
fun nativeComposeWindow(
|
fun nativeComposeWindow(
|
||||||
@@ -387,6 +323,14 @@ fun nativeComposeWindow(
|
|||||||
height: Int = 600,
|
height: Int = 600,
|
||||||
gpu: GpuMode = GpuMode.Auto,
|
gpu: GpuMode = GpuMode.Auto,
|
||||||
icon: AppWindowIcon? = null,
|
icon: AppWindowIcon? = null,
|
||||||
|
undecorated: Boolean = false,
|
||||||
|
transparent: Boolean = false,
|
||||||
|
resizable: Boolean = true,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
focusable: Boolean = true,
|
||||||
|
alwaysOnTop: Boolean = false,
|
||||||
|
onPreviewKeyEvent: (KeyEvent) -> Boolean = { false },
|
||||||
|
onKeyEvent: (KeyEvent) -> Boolean = { false },
|
||||||
onFrame: ((backend: RenderBackend, frameIndex: Int) -> Boolean)? = null,
|
onFrame: ((backend: RenderBackend, frameIndex: Int) -> Boolean)? = null,
|
||||||
content: @Composable ComposeWindowScope.() -> Unit,
|
content: @Composable ComposeWindowScope.() -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -398,592 +342,16 @@ fun nativeComposeWindow(
|
|||||||
height = height,
|
height = height,
|
||||||
gpu = gpu,
|
gpu = gpu,
|
||||||
icon = icon,
|
icon = icon,
|
||||||
|
undecorated = undecorated,
|
||||||
|
transparent = transparent,
|
||||||
|
resizable = resizable,
|
||||||
|
enabled = enabled,
|
||||||
|
focusable = focusable,
|
||||||
|
alwaysOnTop = alwaysOnTop,
|
||||||
|
onPreviewKeyEvent = onPreviewKeyEvent,
|
||||||
|
onKeyEvent = onKeyEvent,
|
||||||
onFrame = onFrame,
|
onFrame = onFrame,
|
||||||
content = content,
|
content = content,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================
|
|
||||||
// MARK: App runtime internals
|
|
||||||
// ==================
|
|
||||||
|
|
||||||
internal class ApplicationScopeImpl(val runtime: AppRuntime) : ApplicationScope {
|
|
||||||
override fun exitApplication() { runtime.exitRequested = true }
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Registry + lifecycle for the live windows. Windows are created by the app
|
|
||||||
composition (Window()'s remember) and destroyed by the LOOP - DisposableEffect
|
|
||||||
onDispose only schedules, because teardown disposes a composition and that
|
|
||||||
must not run re-entrantly inside another composition's apply pass. */
|
|
||||||
internal class AppRuntime {
|
|
||||||
lateinit var scope: CoroutineScope
|
|
||||||
val windows = mutableListOf<WindowInstance>()
|
|
||||||
private val destroyQueue = mutableListOf<WindowInstance>()
|
|
||||||
var exitRequested = false
|
|
||||||
var hadWindow = false
|
|
||||||
|
|
||||||
fun windowFor(inId: UInt): WindowInstance? =
|
|
||||||
if (inId == 0u) windows.firstOrNull()
|
|
||||||
else windows.firstOrNull { it.backend.windowId == inId } ?: windows.firstOrNull()
|
|
||||||
|
|
||||||
fun markAllNeedFrame() {
|
|
||||||
for (vW in windows) vW.needsFrame = true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createWindow(
|
|
||||||
inTitle: String,
|
|
||||||
inWidth: Int,
|
|
||||||
inHeight: Int,
|
|
||||||
inGpu: GpuMode,
|
|
||||||
inIcon: AppWindowIcon?,
|
|
||||||
inOnFrame: ((RenderBackend, Int) -> Boolean)?,
|
|
||||||
inContent: () -> (@Composable ComposeWindowScope.() -> Unit),
|
|
||||||
): WindowInstance {
|
|
||||||
val vWindow = WindowInstance(inTitle, inWidth, inHeight, inGpu, inIcon, inOnFrame, inContent)
|
|
||||||
// A Window() was declared either way - the "exit when the last window
|
|
||||||
// is gone" rule must also fire when every declared window failed to
|
|
||||||
// initialise (otherwise the loop would spin forever with none).
|
|
||||||
hadWindow = true
|
|
||||||
if (vWindow.init(scope)) {
|
|
||||||
windows.add(vWindow)
|
|
||||||
} else {
|
|
||||||
println("nativeComposeApp: window '$inTitle' failed to initialise")
|
|
||||||
}
|
|
||||||
return vWindow
|
|
||||||
}
|
|
||||||
|
|
||||||
fun scheduleDestroy(inWindow: WindowInstance) {
|
|
||||||
if (inWindow in windows && inWindow !in destroyQueue) destroyQueue.add(inWindow)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun reapDestroyed() {
|
|
||||||
while (destroyQueue.isNotEmpty()) {
|
|
||||||
val vW = destroyQueue.removeLast()
|
|
||||||
windows.remove(vW)
|
|
||||||
vW.destroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// No-op applier for the app-level composition - it emits no UI nodes, only
|
|
||||||
// side effects (each Window() manages a native window).
|
|
||||||
private class UnitApplier : AbstractApplier<Unit>(Unit) {
|
|
||||||
override fun insertTopDown(index: Int, instance: Unit) {}
|
|
||||||
override fun insertBottomUp(index: Int, instance: Unit) {}
|
|
||||||
override fun remove(index: Int, count: Int) {}
|
|
||||||
override fun move(from: Int, to: Int, count: Int) {}
|
|
||||||
override fun onClear() {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================
|
|
||||||
// MARK: WindowInstance - one SDL window + renderer + composition
|
|
||||||
// ==================
|
|
||||||
|
|
||||||
internal class WindowInstance(
|
|
||||||
private val initialTitle: String,
|
|
||||||
inWidth: Int,
|
|
||||||
inHeight: Int,
|
|
||||||
inGpu: GpuMode,
|
|
||||||
inIcon: AppWindowIcon?,
|
|
||||||
private val onFrame: ((RenderBackend, Int) -> Boolean)?,
|
|
||||||
private val contentHolder: () -> (@Composable ComposeWindowScope.() -> Unit),
|
|
||||||
) {
|
|
||||||
// Resolved renderer mode. `var` so init() can fall back GPU → CPU raster when
|
|
||||||
// a Skia GPU context can't be created (RDP / headless / missing GL driver).
|
|
||||||
private var gpuMode = if (inGpu is GpuMode.Auto) rendererPreferredGpuMode() else inGpu
|
|
||||||
private val initialWidth = inWidth
|
|
||||||
private val initialHeight = inHeight
|
|
||||||
private val icon = inIcon
|
|
||||||
// Assigned by init() from the first backend that comes up (see makeBackend()).
|
|
||||||
lateinit var backend: SDL3Backend
|
|
||||||
private set
|
|
||||||
private var renderBackend: RenderBackend? = null
|
|
||||||
lateinit var host: ComposeRootHost
|
|
||||||
private set
|
|
||||||
lateinit var facade: ComposeNativeWindow
|
|
||||||
private set
|
|
||||||
private var popupHost: com.compose.sdl.window.PopupHostState? = null
|
|
||||||
|
|
||||||
val frameClock = SDL3FrameClock()
|
|
||||||
private var recomposer: Recomposer? = null
|
|
||||||
private var recomposeJob: Job? = null
|
|
||||||
private var composition: Composition? = null
|
|
||||||
|
|
||||||
// Set by Window(); the compat wrapper points it at exitApplication().
|
|
||||||
var onCloseRequest: () -> Unit = {}
|
|
||||||
var closeDispatched = false
|
|
||||||
|
|
||||||
// Render-on-demand flag (see the pre-multi-window loop): set by events,
|
|
||||||
// state writes (global observer), resize, expose.
|
|
||||||
var needsFrame = true
|
|
||||||
|
|
||||||
// Hover refresh: last pointer position in PIXEL space, re-dispatched as a
|
|
||||||
// synthetic Move after layout so items scrolled under the cursor hover.
|
|
||||||
private var lastMouseX = -1f
|
|
||||||
private var lastMouseY = -1f
|
|
||||||
private var hasMousePos = false
|
|
||||||
|
|
||||||
private var frameIndex = 0
|
|
||||||
private var fpsEma = 0.0
|
|
||||||
private var fpsLastFrameMs = SDL_GetTicks()
|
|
||||||
private var fpsLastTitleMs = 0uL
|
|
||||||
|
|
||||||
// Escape → back (upstream desktop's BackNavigationEventInput): unconsumed
|
|
||||||
// Escape completes a back navigation on THIS window's dispatcher.
|
|
||||||
private val navigationEventOwner = object : androidx.navigationevent.NavigationEventDispatcherOwner {
|
|
||||||
override val navigationEventDispatcher = androidx.navigationevent.NavigationEventDispatcher()
|
|
||||||
}
|
|
||||||
private val backNavigationInput = BackNavigationInput()
|
|
||||||
|
|
||||||
// WINDOW-scoped architecture-components owner (Lifecycle + ViewModelStore +
|
|
||||||
// SavedStateRegistry) - the same trio upstream desktop's
|
|
||||||
// DefaultArchitectureComponentsOwner supplies through the window
|
|
||||||
// PlatformContext (compose/ui skikoMain PlatformOwnerProvider.skiko.kt).
|
|
||||||
// viewModel(), SavedStateHandle plumbing and navigation3's
|
|
||||||
// rememberViewModelStoreNavEntryDecorator all resolve their parent owners
|
|
||||||
// from this; destroy() moves it to DESTROYED and clears the store.
|
|
||||||
private val architectureOwner = WindowArchitectureOwner()
|
|
||||||
|
|
||||||
// Window focus / visibility → Lifecycle.State, Compose Desktop's mapping:
|
|
||||||
// focused → RESUMED, visible unfocused → STARTED, hidden/minimised →
|
|
||||||
// CREATED. Starts focused+visible (SDL fires FOCUS_GAINED right after
|
|
||||||
// creation anyway; headless probe runs simply stay RESUMED).
|
|
||||||
private var windowFocused = true
|
|
||||||
private var windowVisible = true
|
|
||||||
|
|
||||||
fun onActivationEvent(inEvent: AppEvent.WindowActivation) {
|
|
||||||
inEvent.focused?.let { windowFocused = it }
|
|
||||||
inEvent.visible?.let { windowVisible = it }
|
|
||||||
if (::host.isInitialized) host.setWindowFocused(windowFocused)
|
|
||||||
architectureOwner.setLifecycleState(
|
|
||||||
when {
|
|
||||||
!windowVisible -> androidx.lifecycle.Lifecycle.State.CREATED
|
|
||||||
windowFocused -> androidx.lifecycle.Lifecycle.State.RESUMED
|
|
||||||
else -> androidx.lifecycle.Lifecycle.State.STARTED
|
|
||||||
}
|
|
||||||
)
|
|
||||||
// Shown / restored / focus-gained also invalidate the contents - keep
|
|
||||||
// the pre-lifecycle RedrawNeeded behaviour of these SDL events.
|
|
||||||
needsFrame = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creates + initialises an SDL3Backend for [mode] and its RenderBackend. On
|
|
||||||
// success commits `backend` and returns the renderer; else tears everything
|
|
||||||
// down and returns null so the caller can retry with a different mode.
|
|
||||||
private fun makeBackend(mode: GpuMode): RenderBackend? {
|
|
||||||
val vBackend = SDL3Backend(
|
|
||||||
initialTitle, initialWidth, initialHeight, gpuMode = mode,
|
|
||||||
iconLightResourcePaths = icon?.light ?: emptyList(),
|
|
||||||
iconDarkResourcePaths = icon?.dark ?: emptyList(),
|
|
||||||
)
|
|
||||||
if (!vBackend.init()) {
|
|
||||||
vBackend.destroy(inQuitSdl = false)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
vBackend.updateWindowSize()
|
|
||||||
val vRender = createRenderBackend(vBackend, mode)
|
|
||||||
if (vRender == null || !vRender.ensureSize(vBackend.pixelWidth, vBackend.pixelHeight)) {
|
|
||||||
vRender?.destroy()
|
|
||||||
vBackend.destroy(inQuitSdl = false)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
backend = vBackend
|
|
||||||
return vRender
|
|
||||||
}
|
|
||||||
|
|
||||||
fun init(inScope: CoroutineScope): Boolean {
|
|
||||||
// Try the resolved gpuMode; if it's a Skia GPU mode whose context/bridge
|
|
||||||
// can't come up (RDP / headless / missing GL driver), fall back once to
|
|
||||||
// CPU raster (Software) so the window still opens instead of failing.
|
|
||||||
var vRender = makeBackend(gpuMode)
|
|
||||||
if (vRender == null && gpuMode is GpuMode.Skia) {
|
|
||||||
println("GPU renderer ($gpuMode) unavailable - falling back to CPU raster (Software)")
|
|
||||||
gpuMode = GpuMode.Software
|
|
||||||
vRender = makeBackend(gpuMode)
|
|
||||||
}
|
|
||||||
if (vRender == null) {
|
|
||||||
println("Failed to init render backend")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
renderBackend = vRender
|
|
||||||
|
|
||||||
host = ComposeRootHost(inDensity = backend.pixelDensity)
|
|
||||||
host.attach()
|
|
||||||
// A layer whose content changed (OwnedLayer.invalidate) schedules a frame
|
|
||||||
// even when nothing else (recompose / relayout) is pending - retained layers
|
|
||||||
// need this so a draw-only state change still repaints.
|
|
||||||
host.setInvalidationCallback { needsFrame = true }
|
|
||||||
facade = ComposeNativeWindow(backend, gpuMode, initialTitle)
|
|
||||||
navigationEventOwner.navigationEventDispatcher.addInput(backNavigationInput)
|
|
||||||
|
|
||||||
// The render-bridge globals must point at THIS window's renderer while
|
|
||||||
// its content composes/measures (first composition happens inside
|
|
||||||
// setContent below).
|
|
||||||
installGlobals()
|
|
||||||
|
|
||||||
// Effect context for this window's composition - the screenshot flag injects the
|
|
||||||
// infinite-animation-cancelling policy (see disableInfiniteAnimations above).
|
|
||||||
var vEffectContext = inScope.coroutineContext + frameClock
|
|
||||||
if (disableInfiniteAnimations) vEffectContext += CancelInfiniteAnimationsPolicy
|
|
||||||
val vRecomposer = Recomposer(vEffectContext)
|
|
||||||
recomposer = vRecomposer
|
|
||||||
recomposeJob = inScope.launch(frameClock) { vRecomposer.runRecomposeAndApplyChanges() }
|
|
||||||
val vComposition = Composition(host.applier, vRecomposer)
|
|
||||||
composition = vComposition
|
|
||||||
|
|
||||||
val vUriHandler = object : androidx.compose.ui.platform.UriHandler {
|
|
||||||
override fun openUri(uri: String) { openUrl(uri) }
|
|
||||||
}
|
|
||||||
val vPopupHost = createPopupHostState()
|
|
||||||
popupHost = vPopupHost
|
|
||||||
val vWindowScope = object : ComposeWindowScope {
|
|
||||||
override val window: ComposeNativeWindow = facade
|
|
||||||
}
|
|
||||||
|
|
||||||
vComposition.setContent {
|
|
||||||
CompositionLocalProvider(
|
|
||||||
LocalComposeNativeWindow provides facade,
|
|
||||||
LocalPopupHost provides vPopupHost,
|
|
||||||
// Upstream vendored CompositionLocals.kt declares each of these as
|
|
||||||
// `staticCompositionLocalOf<T> { noLocalProvidedFor("…") }` - reading
|
|
||||||
// one without a Provider throws. Seed them all from the ComposeOwner.
|
|
||||||
androidx.compose.ui.platform.LocalDensity provides host.density,
|
|
||||||
androidx.compose.ui.platform.LocalLayoutDirection provides host.layoutDirection,
|
|
||||||
androidx.compose.ui.platform.LocalFocusManager provides host.focusManager,
|
|
||||||
androidx.compose.ui.platform.LocalGraphicsContext provides host.graphicsContext,
|
|
||||||
androidx.compose.ui.platform.LocalViewConfiguration provides host.viewConfiguration,
|
|
||||||
androidx.compose.ui.platform.LocalInputModeManager provides host.inputModeManager,
|
|
||||||
androidx.compose.ui.platform.LocalHapticFeedback provides host.hapticFeedback,
|
|
||||||
androidx.compose.ui.platform.LocalTextToolbar provides host.textToolbar,
|
|
||||||
androidx.compose.ui.platform.LocalWindowInfo provides host.windowInfo,
|
|
||||||
androidx.compose.ui.platform.LocalSoftwareKeyboardController provides host.softwareKeyboardController,
|
|
||||||
androidx.compose.ui.platform.LocalClipboard provides
|
|
||||||
androidx.compose.ui.platform.platformClipboard(),
|
|
||||||
androidx.compose.ui.platform.LocalFontFamilyResolver provides
|
|
||||||
com.compose.sdl.text.font.projectFontFamilyResolver,
|
|
||||||
androidx.compose.ui.platform.LocalUriHandler provides vUriHandler,
|
|
||||||
// Desktop windows have no system bars / notch / IME insets - the
|
|
||||||
// interface's all-zero defaults are exactly right.
|
|
||||||
androidx.compose.ui.platform.LocalPlatformWindowInsets provides
|
|
||||||
object : androidx.compose.ui.platform.PlatformWindowInsets {},
|
|
||||||
// The window's architecture-components owner backs all three arch
|
|
||||||
// locals, exactly like upstream desktop's window PlatformContext:
|
|
||||||
// - LocalLifecycleOwner: RESUMED for the window's whole life;
|
|
||||||
// lifecycle-aware content (NavDisplay, rememberLifecycleOwner, …)
|
|
||||||
// errors without it.
|
|
||||||
// - LocalViewModelStoreOwner: window-scoped ViewModels (google
|
|
||||||
// lifecycle-viewmodel-compose's local is a plain composition
|
|
||||||
// local; the JB HostDefault route doesn't exist in the google
|
|
||||||
// artifacts this port ships).
|
|
||||||
// - LocalSavedStateRegistryOwner: SavedStateHandle / rememberSaveable
|
|
||||||
// registry parent (nav3's ViewModelStoreNavEntryDecorator requires it).
|
|
||||||
androidx.lifecycle.compose.LocalLifecycleOwner provides architectureOwner,
|
|
||||||
androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner provides architectureOwner,
|
|
||||||
androidx.savedstate.compose.LocalSavedStateRegistryOwner provides architectureOwner,
|
|
||||||
// Runtime-level host defaults - this window's navigation-event
|
|
||||||
// owner (SearchBar / sheets back plumbing). The viewmodel store
|
|
||||||
// is provided through the plain LocalViewModelStoreOwner above
|
|
||||||
// instead: the google lifecycle artifacts this port ships have no
|
|
||||||
// HostDefault key for it (that mechanism is JB-variant-only).
|
|
||||||
androidx.compose.runtime.LocalHostDefaultProvider provides
|
|
||||||
remember {
|
|
||||||
object : androidx.compose.runtime.HostDefaultProvider {
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
override fun <T> getHostDefault(key: androidx.compose.runtime.HostDefaultKey<T>): T = when (key) {
|
|
||||||
androidx.navigationevent.compose.NavigationEventDispatcherOwnerHostDefaultKey -> navigationEventOwner
|
|
||||||
else -> null
|
|
||||||
} as T
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
// Seed the :ui-internal LocalPointerIconService so Modifier.pointerHoverIcon
|
|
||||||
// drives the SDL cursor (the local + service type can't cross the module boundary).
|
|
||||||
host.ProvidePointerIconService {
|
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
|
||||||
// Read through the holder so Window() recompositions with a
|
|
||||||
// new content lambda propagate into this composition.
|
|
||||||
val vContent = contentHolder()
|
|
||||||
with(vWindowScope) { vContent() }
|
|
||||||
PopupLayer(vPopupHost)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// First composition done (setContent is synchronous) - promote the
|
|
||||||
// window lifecycle from CREATED to the focus/visibility-derived state.
|
|
||||||
// Composition itself runs at CREATED so enableSavedStateHandles()
|
|
||||||
// callers see a legal state, mirroring upstream desktop's
|
|
||||||
// compose-first-resume-after ordering.
|
|
||||||
architectureOwner.setLifecycleState(
|
|
||||||
when {
|
|
||||||
!windowVisible -> androidx.lifecycle.Lifecycle.State.CREATED
|
|
||||||
windowFocused -> androidx.lifecycle.Lifecycle.State.RESUMED
|
|
||||||
else -> androidx.lifecycle.Lifecycle.State.STARTED
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Points the render-bridge globals (text measurer / image loader /
|
|
||||||
viewport) at this window's renderer. Must be called before composing,
|
|
||||||
measuring, or drawing this window's tree. */
|
|
||||||
fun installGlobals() {
|
|
||||||
val vRender = renderBackend ?: return
|
|
||||||
currentImageLoader = vRender.imageLoader
|
|
||||||
com.compose.sdl.text.currentViewportWidth = backend.pixelWidth
|
|
||||||
com.compose.sdl.text.currentViewportHeight = backend.pixelHeight
|
|
||||||
// Register bundled generic fonts (FontFamily.Monospace → NotoSansMono) once
|
|
||||||
// data.kres is loadable; idempotent, no-op if the font isn't bundled.
|
|
||||||
com.compose.sdl.text.registerGenericFonts()
|
|
||||||
// Position the OS IME candidate window the moment a field in THIS window
|
|
||||||
// gains focus, not only on the first TEXT_EDITING event (repointed per
|
|
||||||
// window here so multi-window focus targets the right SDL window).
|
|
||||||
com.compose.sdl.text.input.ImeBridge.onSessionActiveChange = { active ->
|
|
||||||
if (active) updateImeArea()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============
|
|
||||||
// Event handling (loop-routed, per window)
|
|
||||||
|
|
||||||
fun onPointerEvent(inEvent: AppEvent.Pointer) {
|
|
||||||
needsFrame = true
|
|
||||||
installGlobals()
|
|
||||||
val vType = when (inEvent.event.type) {
|
|
||||||
PointerEventType.Press -> 1
|
|
||||||
PointerEventType.Release -> 2
|
|
||||||
else -> 0
|
|
||||||
}
|
|
||||||
val vBtn = when (inEvent.event.button) {
|
|
||||||
PointerButton.Secondary -> 1
|
|
||||||
PointerButton.Tertiary -> 2
|
|
||||||
else -> 0
|
|
||||||
}
|
|
||||||
// SDL3 delivers mouse coords in logical points on HiDPI - multiply by
|
|
||||||
// DPR so hit-testing lands in the pixel space layout uses.
|
|
||||||
val vDpr = backend.pixelDensity
|
|
||||||
val vPx = inEvent.event.x * vDpr
|
|
||||||
val vPy = inEvent.event.y * vDpr
|
|
||||||
if (inEvent.event.type == PointerEventType.Press) {
|
|
||||||
popupHost?.notifyOutsidePress(vPx.toInt(), vPy.toInt())
|
|
||||||
}
|
|
||||||
host.onPointerRaw(vPx, vPy, vType, vBtn, SDL_GetTicks().toLong())
|
|
||||||
lastMouseX = vPx
|
|
||||||
lastMouseY = vPy
|
|
||||||
hasMousePos = true
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Pointer left the window (SDL_EVENT_WINDOW_MOUSE_LEAVE). Stop the per-frame
|
|
||||||
synthetic hover re-dispatch and fire one Exit at the last position so a
|
|
||||||
hovered widget clears its highlight instead of sticking forever. */
|
|
||||||
fun onPointerExit() {
|
|
||||||
needsFrame = true
|
|
||||||
if (hasMousePos) {
|
|
||||||
host.onPointerRaw(lastMouseX, lastMouseY, 3, 0, SDL_GetTicks().toLong())
|
|
||||||
}
|
|
||||||
hasMousePos = false
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onWheelEvent(inEvent: AppEvent.MouseWheel) {
|
|
||||||
needsFrame = true
|
|
||||||
installGlobals()
|
|
||||||
val vDpr = backend.pixelDensity
|
|
||||||
host.onWheel(inEvent.x * vDpr, inEvent.y * vDpr, inEvent.deltaX, inEvent.deltaY, SDL_GetTicks().toLong())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onKeyEvent(inEvent: AppEvent.Key) {
|
|
||||||
needsFrame = true
|
|
||||||
installGlobals()
|
|
||||||
// Focused chain → window-level shortcuts → back navigation (Escape).
|
|
||||||
if (!host.dispatchKeyEvent(inEvent.event) && !facade.dispatchKeyShortcut(inEvent.event)) {
|
|
||||||
backNavigationInput.onKeyEvent(inEvent.event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onTextInputEvent(inEvent: AppEvent.TextInput) {
|
|
||||||
needsFrame = true
|
|
||||||
installGlobals()
|
|
||||||
// A focused text field runs an IME session (ComposeOwner.textInputSession):
|
|
||||||
// commit through it so the text REPLACES any active composition. Falls back
|
|
||||||
// to synthesising typed KeyEvents when no field is focused - SDL key events
|
|
||||||
// carry UNSHIFTED keycodes (no uppercase/numpad/dead keys), so committed
|
|
||||||
// text is the only layout-correct character source.
|
|
||||||
if (!com.compose.sdl.text.input.ImeBridge.commit(inEvent.text)) {
|
|
||||||
dispatchTypedText(host, inEvent.text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onTextEditingEvent(inEvent: AppEvent.TextEditing) {
|
|
||||||
needsFrame = true
|
|
||||||
installGlobals()
|
|
||||||
// IME preedit: show the composing (underlined) region in the focused field.
|
|
||||||
// No-op if no field is focused. Also nudge the OS candidate window to the
|
|
||||||
// field's on-screen rect.
|
|
||||||
com.compose.sdl.text.input.ImeBridge.compose(inEvent.text)
|
|
||||||
updateImeArea()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nudge the OS IME candidate window to the focused field's on-screen rect.
|
|
||||||
// Best-effort: no-op without an active session or a laid-out field.
|
|
||||||
@OptIn(androidx.compose.ui.ExperimentalComposeUiApi::class, kotlinx.cinterop.ExperimentalForeignApi::class)
|
|
||||||
private fun updateImeArea() {
|
|
||||||
val vRequest = com.compose.sdl.text.input.ImeBridge.request ?: return
|
|
||||||
val vRect = vRequest.focusedRectInRoot() ?: return
|
|
||||||
val vWindow = backend.window ?: return
|
|
||||||
val vDpr = backend.pixelDensity
|
|
||||||
kotlinx.cinterop.memScoped {
|
|
||||||
// SDL wants the area in logical window points; layout runs in physical px.
|
|
||||||
val vSdlRect = alloc<sdl3.SDL_Rect>()
|
|
||||||
vSdlRect.x = (vRect.left / vDpr).toInt()
|
|
||||||
vSdlRect.y = (vRect.top / vDpr).toInt()
|
|
||||||
vSdlRect.w = (vRect.width / vDpr).toInt().coerceAtLeast(1)
|
|
||||||
vSdlRect.h = (vRect.height / vDpr).toInt().coerceAtLeast(1)
|
|
||||||
sdl3.SDL_SetTextInputArea(vWindow.reinterpret(), vSdlRect.ptr, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onDropEvent(inEvent: AppEvent.Drop) {
|
|
||||||
needsFrame = true
|
|
||||||
installGlobals()
|
|
||||||
// SDL fires drop coords in logical points at DPR-1; scale to the pixel
|
|
||||||
// space layout runs in (Option-B density flow) so hit-testing lands
|
|
||||||
// where the pointer is.
|
|
||||||
val vDpr = backend.pixelDensity
|
|
||||||
when (inEvent.phase) {
|
|
||||||
AppEvent.DropPhase.BEGIN -> host.onDropBegin()
|
|
||||||
AppEvent.DropPhase.POSITION -> host.onDropPosition(inEvent.x * vDpr, inEvent.y * vDpr)
|
|
||||||
AppEvent.DropPhase.FILE -> inEvent.data?.let { host.onDropFile(it) }
|
|
||||||
AppEvent.DropPhase.TEXT -> inEvent.data?.let { host.onDropText(it) }
|
|
||||||
AppEvent.DropPhase.COMPLETE -> host.onDropComplete()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onResizedEvent() {
|
|
||||||
needsFrame = true
|
|
||||||
backend.updateWindowSize()
|
|
||||||
facade.onResized()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** OS light/dark theme changed - re-pick the theme-appropriate window icon
|
|
||||||
(no-op if this window has no icon configured or the choice is unchanged). */
|
|
||||||
fun onSystemThemeChanged() {
|
|
||||||
backend.applyThemeIcon()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** OS close button / app-level Quit: honour the veto handler, then hand the
|
|
||||||
decision to the Window() declaration. */
|
|
||||||
fun requestClose() {
|
|
||||||
if (closeDispatched) return
|
|
||||||
if (facade.requestCloseFromUser()) {
|
|
||||||
closeDispatched = true
|
|
||||||
onCloseRequest()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============
|
|
||||||
// Frame pump
|
|
||||||
|
|
||||||
fun shouldRender(): Boolean =
|
|
||||||
needsFrame || (recomposer?.hasPendingWork == true) || onFrame != null || kForceRender
|
|
||||||
|
|
||||||
/** True while this window still has pending work: a state/layout/draw invalidation
|
|
||||||
(needsFrame), recomposer work, or a composition/node animation awaiting the next
|
|
||||||
frame. Sampled by windowHasInvalidations() from onFrame probes - the quiescence
|
|
||||||
signal for render-to-settle screenshot capture. */
|
|
||||||
fun hasInvalidations(): Boolean =
|
|
||||||
needsFrame || recomposer?.hasPendingWork == true ||
|
|
||||||
frameClock.hasAwaiters || host.hasAnimationAwaiters()
|
|
||||||
|
|
||||||
fun renderFrame() {
|
|
||||||
val vRender = renderBackend ?: return
|
|
||||||
needsFrame = false
|
|
||||||
if (recomposer?.hasPendingWork == true) needsFrame = true
|
|
||||||
|
|
||||||
backend.updateWindowSize()
|
|
||||||
vRender.ensureSize(backend.pixelWidth, backend.pixelHeight)
|
|
||||||
// Charge the GPU back-buffer acquire to its own phase. On Metal this is
|
|
||||||
// where nextDrawable() blocks on vsync (the natural frame pacing), so
|
|
||||||
// folding it into "layout" made the profiler read the vsync wait as
|
|
||||||
// layout cost - a ~6-7ms phantom that hid the real (tiny) layout time.
|
|
||||||
FrameProfiler.phase(" acquire")
|
|
||||||
host.setConstraints(backend.pixelWidth, backend.pixelHeight)
|
|
||||||
// Deliver any state written by this iteration's frame-clock continuations
|
|
||||||
// (withFrameNanos animations - notably smooth wheel scrolling) BEFORE we lay
|
|
||||||
// out. Those writes happen in the per-window pump's sendFrame()/yield(), which
|
|
||||||
// is AFTER the loop's last sendApplyNotifications() - so without this the
|
|
||||||
// relayout they trigger wasn't registered until the next frame, and the frame
|
|
||||||
// drew last frame's positions. That one-frame trail read as "the clip is a
|
|
||||||
// frame late" when scrolling (content briefly drawn past the viewport edge).
|
|
||||||
Snapshot.sendApplyNotifications()
|
|
||||||
host.measureAndLayout()
|
|
||||||
FrameProfiler.phase(" layout")
|
|
||||||
|
|
||||||
// Hover refresh after layout (upstream skiko: SyntheticEventSender).
|
|
||||||
if (hasMousePos) {
|
|
||||||
host.onPointerRaw(lastMouseX, lastMouseY, 0, 0, SDL_GetTicks().toLong())
|
|
||||||
}
|
|
||||||
|
|
||||||
vRender.beginFrame(1f)
|
|
||||||
vRender.drawRoot { canvas -> host.drawRoot(canvas) }
|
|
||||||
if (onFrame != null) {
|
|
||||||
renderingWindow = this
|
|
||||||
val vContinue = onFrame.invoke(vRender, frameIndex)
|
|
||||||
renderingWindow = null
|
|
||||||
// Probe consumers end the app when their scenario completes.
|
|
||||||
if (!vContinue) facade.close()
|
|
||||||
}
|
|
||||||
FrameProfiler.phase(" draw")
|
|
||||||
vRender.endFrame()
|
|
||||||
FrameProfiler.phase(" present")
|
|
||||||
frameIndex++
|
|
||||||
|
|
||||||
// FPS - instantaneous inter-frame rate, EMA-smoothed, title refreshed
|
|
||||||
// ~4x/sec. This shows within ~2 rendered frames of ANY activity rather
|
|
||||||
// than waiting for a full second of unbroken rendering to accumulate
|
|
||||||
// (the old fixed-window counter never got its first update: bursty
|
|
||||||
// interaction kept idling before 1s elapsed, and the idle-skip reset the
|
|
||||||
// window each time - so FPS only appeared during a long enough page
|
|
||||||
// transition). A dt outside 1..100ms is the first frame or a resume from
|
|
||||||
// idle (the gap isn't a real frame interval), so it's not sampled - the
|
|
||||||
// title just holds the last active rate while idle.
|
|
||||||
val vNowMs = SDL_GetTicks()
|
|
||||||
val vDt = (vNowMs - fpsLastFrameMs).toInt()
|
|
||||||
fpsLastFrameMs = vNowMs
|
|
||||||
if (vDt in 1..100) {
|
|
||||||
val vInst = 1000.0 / vDt
|
|
||||||
fpsEma = if (fpsEma <= 0.0) vInst else fpsEma * 0.9 + vInst * 0.1
|
|
||||||
if ((vNowMs - fpsLastTitleMs).toInt() >= 250) {
|
|
||||||
val vFps = (fpsEma + 0.5).toInt()
|
|
||||||
facade.updateFps(vFps)
|
|
||||||
SDL_SetWindowTitle(backend.window?.reinterpret(), "${facade.title} · $vFps FPS")
|
|
||||||
fpsLastTitleMs = vNowMs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Idle-skip calls this: drop the frame timer so the resume-from-idle frame's
|
|
||||||
// dt (the whole idle gap) isn't sampled as a real interval. The EMA is left
|
|
||||||
// intact so the title keeps showing the last active rate while idle.
|
|
||||||
fun resetFpsWindow() {
|
|
||||||
fpsLastFrameMs = SDL_GetTicks()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun destroy() {
|
|
||||||
composition?.dispose()
|
|
||||||
composition = null
|
|
||||||
// Window gone → DESTROYED lifecycle + ViewModels cleared (onCleared
|
|
||||||
// runs), matching upstream desktop's window-scoped owner lifetime.
|
|
||||||
architectureOwner.destroy()
|
|
||||||
recomposer?.cancel()
|
|
||||||
recomposer = null
|
|
||||||
recomposeJob?.cancel()
|
|
||||||
recomposeJob = null
|
|
||||||
renderBackend?.destroy()
|
|
||||||
renderBackend = null
|
|
||||||
backend.destroy(inQuitSdl = false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WindowArchitectureOwner -> WindowArchitectureOwner.kt
|
|
||||||
// BackNavigationInput / dispatchTypedText -> WindowInputHelpers.kt
|
|
||||||
|
|||||||
+26
-26
@@ -18,56 +18,56 @@ import sdl3.SDL_GetTicks
|
|||||||
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
|
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
|
||||||
internal object FrameProfiler {
|
internal object FrameProfiler {
|
||||||
// null until first checked; then true/false for the run's lifetime.
|
// null until first checked; then true/false for the run's lifetime.
|
||||||
private var fEnabled: Boolean? = null
|
private var mEnabled: Boolean? = null
|
||||||
// Output file - resolved once from CDN_PROFILE. Writing to a file (not
|
// Output file - resolved once from CDN_PROFILE. Writing to a file (not
|
||||||
// stdout) lets GUI-subsystem apps (the demo links --subsystem,windows, so
|
// stdout) lets GUI-subsystem apps (the demo links --subsystem,windows, so
|
||||||
// it has no console) be profiled too. CDN_PROFILE=1 → "cdn_profile.log" in
|
// it has no console) be profiled too. CDN_PROFILE=1 → "cdn_profile.log" in
|
||||||
// the cwd; CDN_PROFILE=<path> → that path.
|
// the cwd; CDN_PROFILE=<path> → that path.
|
||||||
private var fPath: String = "cdn_profile.log"
|
private var mPath: String = "cdn_profile.log"
|
||||||
val enabled: Boolean
|
val enabled: Boolean
|
||||||
get() = fEnabled ?: run {
|
get() = mEnabled ?: run {
|
||||||
val vEnv = platform.posix.getenv("CDN_PROFILE")?.toKString()
|
val vEnv = platform.posix.getenv("CDN_PROFILE")?.toKString()
|
||||||
if (vEnv != null && vEnv != "1" && vEnv.isNotEmpty()) fPath = vEnv
|
if (vEnv != null && vEnv != "1" && vEnv.isNotEmpty()) mPath = vEnv
|
||||||
(vEnv != null).also { fEnabled = it }
|
(vEnv != null).also { mEnabled = it }
|
||||||
}
|
}
|
||||||
|
|
||||||
private val fFreq = SDL_GetPerformanceFrequency().toDouble()
|
private val mFreq = SDL_GetPerformanceFrequency().toDouble()
|
||||||
// Insertion-ordered so the printed line follows the call order.
|
// Insertion-ordered so the printed line follows the call order.
|
||||||
private val fSum = LinkedHashMap<String, Double>()
|
private val mSum = LinkedHashMap<String, Double>()
|
||||||
private val fMax = LinkedHashMap<String, Double>()
|
private val mMax = LinkedHashMap<String, Double>()
|
||||||
private var fFrames = 0
|
private var mFrames = 0
|
||||||
private var fLastPrintMs = SDL_GetTicks()
|
private var mLastPrintMs = SDL_GetTicks()
|
||||||
private var fMark = 0uL
|
private var mMark = 0uL
|
||||||
|
|
||||||
fun mark() { if (enabled) fMark = SDL_GetPerformanceCounter() }
|
fun mark() { if (enabled) mMark = SDL_GetPerformanceCounter() }
|
||||||
|
|
||||||
fun phase(inName: String) {
|
fun phase(inName: String) {
|
||||||
if (!enabled) return
|
if (!enabled) return
|
||||||
val vNow = SDL_GetPerformanceCounter()
|
val vNow = SDL_GetPerformanceCounter()
|
||||||
val vMs = (vNow - fMark).toDouble() * 1000.0 / fFreq
|
val vMs = (vNow - mMark).toDouble() * 1000.0 / mFreq
|
||||||
fSum[inName] = (fSum[inName] ?: 0.0) + vMs
|
mSum[inName] = (mSum[inName] ?: 0.0) + vMs
|
||||||
if (vMs > (fMax[inName] ?: 0.0)) fMax[inName] = vMs
|
if (vMs > (mMax[inName] ?: 0.0)) mMax[inName] = vMs
|
||||||
fMark = vNow
|
mMark = vNow
|
||||||
}
|
}
|
||||||
|
|
||||||
fun frameDone(inRendered: Boolean) {
|
fun frameDone(inRendered: Boolean) {
|
||||||
if (!enabled) return
|
if (!enabled) return
|
||||||
if (inRendered) fFrames++
|
if (inRendered) mFrames++
|
||||||
val vNowMs = SDL_GetTicks()
|
val vNowMs = SDL_GetTicks()
|
||||||
if (vNowMs - fLastPrintMs >= 2000u && fFrames > 0) {
|
if (vNowMs - mLastPrintMs >= 2000u && mFrames > 0) {
|
||||||
val vParts = fSum.keys.map { vName ->
|
val vParts = mSum.keys.map { vName ->
|
||||||
val vAvg = (fSum[vName] ?: 0.0) / fFrames
|
val vAvg = (mSum[vName] ?: 0.0) / mFrames
|
||||||
"$vName=${(vAvg * 100).toInt() / 100.0}/${((fMax[vName] ?: 0.0) * 100).toInt() / 100.0}ms"
|
"$vName=${(vAvg * 100).toInt() / 100.0}/${((mMax[vName] ?: 0.0) * 100).toInt() / 100.0}ms"
|
||||||
}
|
}
|
||||||
val vLine = "[profile] frames=$fFrames avg/max " + vParts.joinToString(" ") + "\n"
|
val vLine = "[profile] frames=$mFrames avg/max " + vParts.joinToString(" ") + "\n"
|
||||||
val vFile = platform.posix.fopen(fPath, "a")
|
val vFile = platform.posix.fopen(mPath, "a")
|
||||||
if (vFile != null) {
|
if (vFile != null) {
|
||||||
platform.posix.fputs(vLine, vFile)
|
platform.posix.fputs(vLine, vFile)
|
||||||
platform.posix.fclose(vFile)
|
platform.posix.fclose(vFile)
|
||||||
}
|
}
|
||||||
fSum.clear(); fMax.clear()
|
mSum.clear(); mMax.clear()
|
||||||
fFrames = 0
|
mFrames = 0
|
||||||
fLastPrintMs = vNowMs
|
mLastPrintMs = vNowMs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+83
@@ -0,0 +1,83 @@
|
|||||||
|
@file:OptIn(
|
||||||
|
androidx.compose.ui.InternalComposeUiApi::class,
|
||||||
|
androidx.compose.runtime.InternalComposeApi::class,
|
||||||
|
)
|
||||||
|
|
||||||
|
package com.compose.sdl
|
||||||
|
|
||||||
|
import kotlinx.cinterop.COpaquePointer
|
||||||
|
import kotlinx.cinterop.reinterpret
|
||||||
|
import kotlinx.cinterop.pointed
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import sdl3.SDL_GetCurrentDisplayMode
|
||||||
|
import sdl3.SDL_GetDisplayForWindow
|
||||||
|
import sdl3.SDL_GetTicks
|
||||||
|
import sdl3.SDL_GetTicksNS
|
||||||
|
|
||||||
|
// ==================
|
||||||
|
// MARK: Deterministic frame timing + probe/screenshot support
|
||||||
|
// ==================
|
||||||
|
|
||||||
|
/** Set BEFORE nativeComposeApp/nativeComposeWindow: every window's composition then runs
|
||||||
|
under an InfiniteAnimationPolicy that CANCELS infinite animations, so
|
||||||
|
rememberInfiniteTransition & co. freeze at their initial value - the same mechanism
|
||||||
|
upstream's test rules use. Screenshot/parity runs enable it so looping screens can
|
||||||
|
reach quiescence and capture deterministically. */
|
||||||
|
var disableInfiniteAnimations: Boolean = false
|
||||||
|
|
||||||
|
/** The cancelling policy: a coroutine that ends in CancellationException counts as
|
||||||
|
cancelled (not failed), so only the animation coroutine stops - nothing propagates. */
|
||||||
|
internal object CancelInfiniteAnimationsPolicy : androidx.compose.ui.platform.InfiniteAnimationPolicy {
|
||||||
|
override suspend fun <R> onInfiniteOperation(block: suspend () -> R): R =
|
||||||
|
throw CancellationException("infinite animations are disabled (disableInfiniteAnimations)")
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Set BEFORE nativeComposeApp/nativeComposeWindow: the composition + animation frame
|
||||||
|
clocks advance a VIRTUAL 16.6ms per main-loop iteration instead of reading SDL's
|
||||||
|
real-time ticks - the native mirror of the JVM parity leg's render(nanos) stepping.
|
||||||
|
Animations then progress by exact per-frame deltas, so anything time-raced (e.g. a
|
||||||
|
bring-into-view scroll interrupted mid-flight) resolves identically on every run and
|
||||||
|
screenshots become deterministic. Input timestamps and FPS stay on real time. */
|
||||||
|
var useVirtualFrameTime: Boolean = false
|
||||||
|
|
||||||
|
private var virtualFrameNanos = 0L
|
||||||
|
|
||||||
|
internal fun advanceVirtualFrame() {
|
||||||
|
if (useVirtualFrameTime) virtualFrameNanos += 16_666_667L
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timestamp for the composition frame clocks (recomposer + withFrameNanos animations).
|
||||||
|
internal fun frameClockNanos(): Long =
|
||||||
|
if (useVirtualFrameTime) virtualFrameNanos else SDL_GetTicksNS().toLong()
|
||||||
|
|
||||||
|
// Timestamp for the owner's node-animation clock - real path keeps the pre-existing
|
||||||
|
// ms-resolution SDL_GetTicks base so non-screenshot behaviour is bit-for-bit unchanged.
|
||||||
|
internal fun animationClockNanos(): Long =
|
||||||
|
if (useVirtualFrameTime) virtualFrameNanos else SDL_GetTicks().toLong() * 1_000_000L
|
||||||
|
|
||||||
|
// The window currently inside renderFrame - the loop is single-threaded, so a plain
|
||||||
|
// var is enough for onFrame probes to address "the window I'm being called for".
|
||||||
|
internal var renderingWindow: WindowInstance? = null
|
||||||
|
|
||||||
|
/** From inside an onFrame callback: true while the just-rendered window still has pending
|
||||||
|
work (recomposition, layout/draw invalidation, or an animation awaiting the next frame).
|
||||||
|
Screenshot probes capture once this stays false for a few consecutive frames instead of
|
||||||
|
at a fixed frame count. Outside onFrame it answers false. */
|
||||||
|
fun windowHasInvalidations(): Boolean = renderingWindow?.hasInvalidations() ?: false
|
||||||
|
|
||||||
|
/** Trigger a Kotlin/Native GC so Cleaner-managed renderer resources release
|
||||||
|
their native memory (see the main loop's native-memory nudge). */
|
||||||
|
@OptIn(kotlin.native.runtime.NativeRuntimeApi::class)
|
||||||
|
internal fun collectNativeGarbage() = kotlin.native.runtime.GC.collect()
|
||||||
|
|
||||||
|
/** Frame interval (ms) of the window's current display, for the non-vsync
|
||||||
|
fallback pacing (SDL_Delay). Falls back to 16ms (~60Hz) when the mode can't
|
||||||
|
be read, so a 144Hz panel on a non-vsync path isn't capped to 60. */
|
||||||
|
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
|
||||||
|
internal fun displayFrameDelayMs(window: COpaquePointer?): UInt {
|
||||||
|
val vWindow = window ?: return 16u
|
||||||
|
val vDisplay = SDL_GetDisplayForWindow(vWindow.reinterpret())
|
||||||
|
val vMode = SDL_GetCurrentDisplayMode(vDisplay) ?: return 16u
|
||||||
|
val vHz = vMode.pointed.refresh_rate
|
||||||
|
return if (vHz > 0f) (1000f / vHz).toUInt().coerceAtLeast(1u) else 16u
|
||||||
|
}
|
||||||
+1
-1
@@ -20,7 +20,7 @@ import sdl3.SDL_GetTicksNS
|
|||||||
|
|
||||||
Both come from delegating to the runtime's BroadcastFrameClock, which is
|
Both come from delegating to the runtime's BroadcastFrameClock, which is
|
||||||
exactly upstream's dispatch structure. */
|
exactly upstream's dispatch structure. */
|
||||||
internal class SDL3FrameClock : MonotonicFrameClock {
|
internal class Sdl3FrameClock : MonotonicFrameClock {
|
||||||
private val broadcast = BroadcastFrameClock()
|
private val broadcast = BroadcastFrameClock()
|
||||||
|
|
||||||
override suspend fun <R> withFrameNanos(onFrame: (Long) -> R): R =
|
override suspend fun <R> withFrameNanos(onFrame: (Long) -> R): R =
|
||||||
+7
-7
@@ -36,16 +36,16 @@ import sdl3.SDL_GetCurrentThreadID
|
|||||||
writes land at the loop's drainPending(), observable on the same frame. */
|
writes land at the loop's drainPending(), observable on the same frame. */
|
||||||
internal class Sdl3MainDispatcher : MainCoroutineDispatcher() {
|
internal class Sdl3MainDispatcher : MainCoroutineDispatcher() {
|
||||||
|
|
||||||
private val fQueue = Channel<Runnable>(Channel.UNLIMITED)
|
private val mQueue = Channel<Runnable>(Channel.UNLIMITED)
|
||||||
|
|
||||||
// The SDL main thread - the dispatcher is constructed by nativeComposeApp
|
// The SDL main thread - the dispatcher is constructed by nativeComposeApp
|
||||||
// on the thread that then runs the loop.
|
// on the thread that then runs the loop.
|
||||||
private val fMainThreadId = SDL_GetCurrentThreadID()
|
private val mMainThreadId = SDL_GetCurrentThreadID()
|
||||||
|
|
||||||
override val immediate: MainCoroutineDispatcher = ImmediateDispatcher()
|
override val immediate: MainCoroutineDispatcher = ImmediateDispatcher()
|
||||||
|
|
||||||
override fun dispatch(context: CoroutineContext, block: Runnable) {
|
override fun dispatch(context: CoroutineContext, block: Runnable) {
|
||||||
fQueue.trySend(block)
|
mQueue.trySend(block)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Drains all pending tasks. Called from the SDL3 main loop once per
|
/** Drains all pending tasks. Called from the SDL3 main loop once per
|
||||||
@@ -54,7 +54,7 @@ internal class Sdl3MainDispatcher : MainCoroutineDispatcher() {
|
|||||||
composition. */
|
composition. */
|
||||||
fun drainPending() {
|
fun drainPending() {
|
||||||
while (true) {
|
while (true) {
|
||||||
val vTask = fQueue.tryReceive().getOrNull() ?: break
|
val vTask = mQueue.tryReceive().getOrNull() ?: break
|
||||||
runCatching { vTask.run() }.onFailure { vErr ->
|
runCatching { vTask.run() }.onFailure { vErr ->
|
||||||
println("Sdl3MainDispatcher: dispatched task threw: ${vErr.message}")
|
println("Sdl3MainDispatcher: dispatched task threw: ${vErr.message}")
|
||||||
}
|
}
|
||||||
@@ -64,7 +64,7 @@ internal class Sdl3MainDispatcher : MainCoroutineDispatcher() {
|
|||||||
/** Closes the queue. After this, dispatch() drops new posts silently.
|
/** Closes the queue. After this, dispatch() drops new posts silently.
|
||||||
Called from nativeComposeWindow as part of shutdown. */
|
Called from nativeComposeWindow as part of shutdown. */
|
||||||
fun close() {
|
fun close() {
|
||||||
fQueue.close()
|
mQueue.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String = "Sdl3MainDispatcher"
|
override fun toString(): String = "Sdl3MainDispatcher"
|
||||||
@@ -75,10 +75,10 @@ internal class Sdl3MainDispatcher : MainCoroutineDispatcher() {
|
|||||||
override val immediate: MainCoroutineDispatcher get() = this
|
override val immediate: MainCoroutineDispatcher get() = this
|
||||||
|
|
||||||
override fun isDispatchNeeded(context: CoroutineContext): Boolean =
|
override fun isDispatchNeeded(context: CoroutineContext): Boolean =
|
||||||
SDL_GetCurrentThreadID() != fMainThreadId
|
SDL_GetCurrentThreadID() != mMainThreadId
|
||||||
|
|
||||||
override fun dispatch(context: CoroutineContext, block: Runnable) {
|
override fun dispatch(context: CoroutineContext, block: Runnable) {
|
||||||
fQueue.trySend(block)
|
mQueue.trySend(block)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String = "Sdl3MainDispatcher.immediate"
|
override fun toString(): String = "Sdl3MainDispatcher.immediate"
|
||||||
|
|||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
package com.compose.sdl
|
||||||
|
|
||||||
|
// ==================
|
||||||
|
// MARK: WindowAttributes
|
||||||
|
// ==================
|
||||||
|
|
||||||
|
/**
|
||||||
|
The Compose Desktop `Window()` attributes this port supports, bundled so they
|
||||||
|
travel as one value from the composable down to [WindowInstance] instead of as
|
||||||
|
six positional booleans.
|
||||||
|
|
||||||
|
[transparent] is creation-only: SDL needs "SDL_WINDOW_TRANSPARENT" at
|
||||||
|
"SDL_CreateWindow" time and offers no setter, so changing it after the window
|
||||||
|
exists has no effect. The rest are re-applied whenever "Window()" recomposes
|
||||||
|
with a new value.
|
||||||
|
*/
|
||||||
|
internal data class WindowAttributes(
|
||||||
|
/** Hides the title bar and border (`SDL_WINDOW_BORDERLESS`). */
|
||||||
|
val undecorated: Boolean = false,
|
||||||
|
/** Per-pixel alpha for the window surface. CREATION-ONLY - see above. */
|
||||||
|
val transparent: Boolean = false,
|
||||||
|
/** Whether the user can resize the window. */
|
||||||
|
val resizable: Boolean = true,
|
||||||
|
/** False drops input (pointer / key / text / wheel / drop) while the window
|
||||||
|
keeps rendering - Compose Desktop's `enabled = false` behaviour. */
|
||||||
|
val enabled: Boolean = true,
|
||||||
|
/** False stops the window taking keyboard focus when clicked or raised. */
|
||||||
|
val focusable: Boolean = true,
|
||||||
|
/** Keeps the window above other windows. */
|
||||||
|
val alwaysOnTop: Boolean = false,
|
||||||
|
)
|
||||||
+575
@@ -0,0 +1,575 @@
|
|||||||
|
@file:OptIn(
|
||||||
|
androidx.compose.ui.InternalComposeUiApi::class,
|
||||||
|
androidx.compose.runtime.InternalComposeApi::class,
|
||||||
|
)
|
||||||
|
|
||||||
|
package com.compose.sdl
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.Composition
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.Recomposer
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.snapshots.Snapshot
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.input.key.KeyEvent
|
||||||
|
import androidx.compose.ui.input.pointer.PointerButton
|
||||||
|
import androidx.compose.ui.input.pointer.PointerEventType
|
||||||
|
import com.compose.sdl.node.ComposeRootHost
|
||||||
|
import com.compose.sdl.res.currentImageLoader
|
||||||
|
import com.compose.sdl.window.LocalPopupHost
|
||||||
|
import com.compose.sdl.window.PopupLayer
|
||||||
|
import com.compose.sdl.window.createPopupHostState
|
||||||
|
import kotlinx.cinterop.alloc
|
||||||
|
import kotlinx.cinterop.ptr
|
||||||
|
import kotlinx.cinterop.reinterpret
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import sdl3.SDL_GetTicks
|
||||||
|
import sdl3.SDL_SetWindowTitle
|
||||||
|
|
||||||
|
// ==================
|
||||||
|
// MARK: WindowInstance - one SDL window + renderer + composition
|
||||||
|
// ==================
|
||||||
|
|
||||||
|
internal class WindowInstance(
|
||||||
|
private val initialTitle: String,
|
||||||
|
inWidth: Int,
|
||||||
|
inHeight: Int,
|
||||||
|
inGpu: GpuMode,
|
||||||
|
inIcon: AppWindowIcon?,
|
||||||
|
private val attrs: WindowAttributes,
|
||||||
|
private val onFrame: ((RenderBackend, Int) -> Boolean)?,
|
||||||
|
// Window-level key handlers, read through holders so Window() recompositions
|
||||||
|
// with new lambdas take effect without recreating the window.
|
||||||
|
private val previewKeyHandler: () -> ((KeyEvent) -> Boolean),
|
||||||
|
private val keyHandler: () -> ((KeyEvent) -> Boolean),
|
||||||
|
private val contentHolder: () -> (@Composable ComposeWindowScope.() -> Unit),
|
||||||
|
) {
|
||||||
|
// Resolved renderer mode. `var` so init() can fall back GPU → CPU raster when
|
||||||
|
// a Skia GPU context can't be created (RDP / headless / missing GL driver).
|
||||||
|
private var gpuMode = if (inGpu is GpuMode.Auto) rendererPreferredGpuMode() else inGpu
|
||||||
|
private val initialWidth = inWidth
|
||||||
|
private val initialHeight = inHeight
|
||||||
|
private val icon = inIcon
|
||||||
|
// Assigned by init() from the first backend that comes up (see makeBackend()).
|
||||||
|
lateinit var backend: Sdl3Backend
|
||||||
|
private set
|
||||||
|
private var renderBackend: RenderBackend? = null
|
||||||
|
lateinit var host: ComposeRootHost
|
||||||
|
private set
|
||||||
|
lateinit var facade: ComposeNativeWindow
|
||||||
|
private set
|
||||||
|
private var popupHost: com.compose.sdl.window.PopupHostState? = null
|
||||||
|
|
||||||
|
val frameClock = Sdl3FrameClock()
|
||||||
|
private var recomposer: Recomposer? = null
|
||||||
|
private var recomposeJob: Job? = null
|
||||||
|
private var composition: Composition? = null
|
||||||
|
|
||||||
|
// Set by Window(); the compat wrapper points it at exitApplication().
|
||||||
|
var onCloseRequest: () -> Unit = {}
|
||||||
|
var closeDispatched = false
|
||||||
|
|
||||||
|
// Render-on-demand flag (see the pre-multi-window loop): set by events,
|
||||||
|
// state writes (global observer), resize, expose.
|
||||||
|
var needsFrame = true
|
||||||
|
|
||||||
|
// Hover refresh: last pointer position in PIXEL space, re-dispatched as a
|
||||||
|
// synthetic Move after layout so items scrolled under the cursor hover.
|
||||||
|
private var lastMouseX = -1f
|
||||||
|
private var lastMouseY = -1f
|
||||||
|
private var hasMousePos = false
|
||||||
|
|
||||||
|
private var frameIndex = 0
|
||||||
|
private var fpsEma = 0.0
|
||||||
|
private var fpsLastFrameMs = SDL_GetTicks()
|
||||||
|
private var fpsLastTitleMs = 0uL
|
||||||
|
|
||||||
|
// Escape → back (upstream desktop's BackNavigationEventInput): unconsumed
|
||||||
|
// Escape completes a back navigation on THIS window's dispatcher.
|
||||||
|
private val navigationEventOwner = object : androidx.navigationevent.NavigationEventDispatcherOwner {
|
||||||
|
override val navigationEventDispatcher = androidx.navigationevent.NavigationEventDispatcher()
|
||||||
|
}
|
||||||
|
private val backNavigationInput = BackNavigationInput()
|
||||||
|
|
||||||
|
// WINDOW-scoped architecture-components owner (Lifecycle + ViewModelStore +
|
||||||
|
// SavedStateRegistry) - the same trio upstream desktop's
|
||||||
|
// DefaultArchitectureComponentsOwner supplies through the window
|
||||||
|
// PlatformContext (compose/ui skikoMain PlatformOwnerProvider.skiko.kt).
|
||||||
|
// viewModel(), SavedStateHandle plumbing and navigation3's
|
||||||
|
// rememberViewModelStoreNavEntryDecorator all resolve their parent owners
|
||||||
|
// from this; destroy() moves it to DESTROYED and clears the store.
|
||||||
|
private val architectureOwner = WindowArchitectureOwner()
|
||||||
|
|
||||||
|
// Window focus / visibility → Lifecycle.State, Compose Desktop's mapping:
|
||||||
|
// focused → RESUMED, visible unfocused → STARTED, hidden/minimised →
|
||||||
|
// CREATED. Starts focused+visible (SDL fires FOCUS_GAINED right after
|
||||||
|
// creation anyway; headless probe runs simply stay RESUMED).
|
||||||
|
private var windowFocused = true
|
||||||
|
private var windowVisible = true
|
||||||
|
|
||||||
|
fun onActivationEvent(inEvent: AppEvent.WindowActivation) {
|
||||||
|
inEvent.focused?.let { windowFocused = it }
|
||||||
|
inEvent.visible?.let { windowVisible = it }
|
||||||
|
if (::host.isInitialized) host.setWindowFocused(windowFocused)
|
||||||
|
architectureOwner.setLifecycleState(
|
||||||
|
when {
|
||||||
|
!windowVisible -> androidx.lifecycle.Lifecycle.State.CREATED
|
||||||
|
windowFocused -> androidx.lifecycle.Lifecycle.State.RESUMED
|
||||||
|
else -> androidx.lifecycle.Lifecycle.State.STARTED
|
||||||
|
}
|
||||||
|
)
|
||||||
|
// Shown / restored / focus-gained also invalidate the contents - keep
|
||||||
|
// the pre-lifecycle RedrawNeeded behaviour of these SDL events.
|
||||||
|
needsFrame = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates + initialises an Sdl3Backend for [mode] and its RenderBackend. On
|
||||||
|
// success commits `backend` and returns the renderer; else tears everything
|
||||||
|
// down and returns null so the caller can retry with a different mode.
|
||||||
|
private fun makeBackend(mode: GpuMode): RenderBackend? {
|
||||||
|
val vBackend = Sdl3Backend(
|
||||||
|
initialTitle, initialWidth, initialHeight, gpuMode = mode,
|
||||||
|
iconLightResourcePaths = icon?.light ?: emptyList(),
|
||||||
|
iconDarkResourcePaths = icon?.dark ?: emptyList(),
|
||||||
|
undecorated = attrs.undecorated,
|
||||||
|
transparent = attrs.transparent,
|
||||||
|
resizable = attrs.resizable,
|
||||||
|
alwaysOnTop = attrs.alwaysOnTop,
|
||||||
|
focusable = attrs.focusable,
|
||||||
|
)
|
||||||
|
if (!vBackend.init()) {
|
||||||
|
vBackend.destroy(inQuitSdl = false)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
vBackend.updateWindowSize()
|
||||||
|
val vRender = createRenderBackend(vBackend, mode)
|
||||||
|
if (vRender == null || !vRender.ensureSize(vBackend.pixelWidth, vBackend.pixelHeight)) {
|
||||||
|
vRender?.destroy()
|
||||||
|
vBackend.destroy(inQuitSdl = false)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
backend = vBackend
|
||||||
|
return vRender
|
||||||
|
}
|
||||||
|
|
||||||
|
fun init(inScope: CoroutineScope): Boolean {
|
||||||
|
// Try the resolved gpuMode; if it's a Skia GPU mode whose context/bridge
|
||||||
|
// can't come up (RDP / headless / missing GL driver), fall back once to
|
||||||
|
// CPU raster (Software) so the window still opens instead of failing.
|
||||||
|
var vRender = makeBackend(gpuMode)
|
||||||
|
if (vRender == null && gpuMode.isGpuAccelerated) {
|
||||||
|
println("GPU renderer ($gpuMode) unavailable - falling back to CPU raster (Software)")
|
||||||
|
gpuMode = GpuMode.CpuRaster
|
||||||
|
vRender = makeBackend(gpuMode)
|
||||||
|
}
|
||||||
|
if (vRender == null) {
|
||||||
|
println("Failed to init render backend")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
renderBackend = vRender
|
||||||
|
|
||||||
|
host = ComposeRootHost(inDensity = backend.pixelDensity)
|
||||||
|
host.attach()
|
||||||
|
// A layer whose content changed (OwnedLayer.invalidate) schedules a frame
|
||||||
|
// even when nothing else (recompose / relayout) is pending - retained layers
|
||||||
|
// need this so a draw-only state change still repaints.
|
||||||
|
host.setInvalidationCallback { needsFrame = true }
|
||||||
|
facade = ComposeNativeWindow(backend, gpuMode, initialTitle)
|
||||||
|
// Mirror the creation flags into the facade so isResizable & co. read true
|
||||||
|
// from the start and a later Window() update only fires on a real change.
|
||||||
|
applyAttributes(attrs)
|
||||||
|
navigationEventOwner.navigationEventDispatcher.addInput(backNavigationInput)
|
||||||
|
|
||||||
|
// The render-bridge globals must point at THIS window's renderer while
|
||||||
|
// its content composes/measures (first composition happens inside
|
||||||
|
// setContent below).
|
||||||
|
installGlobals()
|
||||||
|
|
||||||
|
// Effect context for this window's composition - the screenshot flag injects the
|
||||||
|
// infinite-animation-cancelling policy (see disableInfiniteAnimations above).
|
||||||
|
var vEffectContext = inScope.coroutineContext + frameClock
|
||||||
|
if (disableInfiniteAnimations) vEffectContext += CancelInfiniteAnimationsPolicy
|
||||||
|
val vRecomposer = Recomposer(vEffectContext)
|
||||||
|
recomposer = vRecomposer
|
||||||
|
recomposeJob = inScope.launch(frameClock) { vRecomposer.runRecomposeAndApplyChanges() }
|
||||||
|
val vComposition = Composition(host.applier, vRecomposer)
|
||||||
|
composition = vComposition
|
||||||
|
|
||||||
|
val vUriHandler = object : androidx.compose.ui.platform.UriHandler {
|
||||||
|
override fun openUri(uri: String) { openUrl(uri) }
|
||||||
|
}
|
||||||
|
val vPopupHost = createPopupHostState()
|
||||||
|
popupHost = vPopupHost
|
||||||
|
val vWindowScope = object : ComposeWindowScope {
|
||||||
|
override val window: ComposeNativeWindow = facade
|
||||||
|
}
|
||||||
|
|
||||||
|
vComposition.setContent {
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalComposeNativeWindow provides facade,
|
||||||
|
LocalPopupHost provides vPopupHost,
|
||||||
|
// Upstream vendored CompositionLocals.kt declares each of these as
|
||||||
|
// `staticCompositionLocalOf<T> { noLocalProvidedFor("…") }` - reading
|
||||||
|
// one without a Provider throws. Seed them all from the ComposeOwner.
|
||||||
|
androidx.compose.ui.platform.LocalDensity provides host.density,
|
||||||
|
androidx.compose.ui.platform.LocalLayoutDirection provides host.layoutDirection,
|
||||||
|
androidx.compose.ui.platform.LocalFocusManager provides host.focusManager,
|
||||||
|
androidx.compose.ui.platform.LocalGraphicsContext provides host.graphicsContext,
|
||||||
|
androidx.compose.ui.platform.LocalViewConfiguration provides host.viewConfiguration,
|
||||||
|
androidx.compose.ui.platform.LocalInputModeManager provides host.inputModeManager,
|
||||||
|
androidx.compose.ui.platform.LocalHapticFeedback provides host.hapticFeedback,
|
||||||
|
androidx.compose.ui.platform.LocalTextToolbar provides host.textToolbar,
|
||||||
|
androidx.compose.ui.platform.LocalWindowInfo provides host.windowInfo,
|
||||||
|
androidx.compose.ui.platform.LocalSoftwareKeyboardController provides host.softwareKeyboardController,
|
||||||
|
androidx.compose.ui.platform.LocalClipboard provides
|
||||||
|
androidx.compose.ui.platform.platformClipboard(),
|
||||||
|
androidx.compose.ui.platform.LocalFontFamilyResolver provides
|
||||||
|
com.compose.sdl.text.font.projectFontFamilyResolver,
|
||||||
|
androidx.compose.ui.platform.LocalUriHandler provides vUriHandler,
|
||||||
|
// Desktop windows have no system bars / notch / IME insets - the
|
||||||
|
// interface's all-zero defaults are exactly right.
|
||||||
|
androidx.compose.ui.platform.LocalPlatformWindowInsets provides
|
||||||
|
object : androidx.compose.ui.platform.PlatformWindowInsets {},
|
||||||
|
// The window's architecture-components owner backs all three arch
|
||||||
|
// locals, exactly like upstream desktop's window PlatformContext:
|
||||||
|
// - LocalLifecycleOwner: RESUMED for the window's whole life;
|
||||||
|
// lifecycle-aware content (NavDisplay, rememberLifecycleOwner, …)
|
||||||
|
// errors without it.
|
||||||
|
// - LocalViewModelStoreOwner: window-scoped ViewModels (google
|
||||||
|
// lifecycle-viewmodel-compose's local is a plain composition
|
||||||
|
// local; the JB HostDefault route doesn't exist in the google
|
||||||
|
// artifacts this port ships).
|
||||||
|
// - LocalSavedStateRegistryOwner: SavedStateHandle / rememberSaveable
|
||||||
|
// registry parent (nav3's ViewModelStoreNavEntryDecorator requires it).
|
||||||
|
androidx.lifecycle.compose.LocalLifecycleOwner provides architectureOwner,
|
||||||
|
androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner provides architectureOwner,
|
||||||
|
androidx.savedstate.compose.LocalSavedStateRegistryOwner provides architectureOwner,
|
||||||
|
// Runtime-level host defaults - this window's navigation-event
|
||||||
|
// owner (SearchBar / sheets back plumbing). The viewmodel store
|
||||||
|
// is provided through the plain LocalViewModelStoreOwner above
|
||||||
|
// instead: the google lifecycle artifacts this port ships have no
|
||||||
|
// HostDefault key for it (that mechanism is JB-variant-only).
|
||||||
|
androidx.compose.runtime.LocalHostDefaultProvider provides
|
||||||
|
remember {
|
||||||
|
object : androidx.compose.runtime.HostDefaultProvider {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun <T> getHostDefault(key: androidx.compose.runtime.HostDefaultKey<T>): T = when (key) {
|
||||||
|
androidx.navigationevent.compose.NavigationEventDispatcherOwnerHostDefaultKey -> navigationEventOwner
|
||||||
|
else -> null
|
||||||
|
} as T
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
// Seed the :ui-internal LocalPointerIconService so Modifier.pointerHoverIcon
|
||||||
|
// drives the SDL cursor (the local + service type can't cross the module boundary).
|
||||||
|
host.ProvidePointerIconService {
|
||||||
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
|
// Read through the holder so Window() recompositions with a
|
||||||
|
// new content lambda propagate into this composition.
|
||||||
|
val vContent = contentHolder()
|
||||||
|
with(vWindowScope) { vContent() }
|
||||||
|
PopupLayer(vPopupHost)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// First composition done (setContent is synchronous) - promote the
|
||||||
|
// window lifecycle from CREATED to the focus/visibility-derived state.
|
||||||
|
// Composition itself runs at CREATED so enableSavedStateHandles()
|
||||||
|
// callers see a legal state, mirroring upstream desktop's
|
||||||
|
// compose-first-resume-after ordering.
|
||||||
|
architectureOwner.setLifecycleState(
|
||||||
|
when {
|
||||||
|
!windowVisible -> androidx.lifecycle.Lifecycle.State.CREATED
|
||||||
|
windowFocused -> androidx.lifecycle.Lifecycle.State.RESUMED
|
||||||
|
else -> androidx.lifecycle.Lifecycle.State.STARTED
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Points the render-bridge globals (text measurer / image loader /
|
||||||
|
viewport) at this window's renderer. Must be called before composing,
|
||||||
|
measuring, or drawing this window's tree. */
|
||||||
|
fun installGlobals() {
|
||||||
|
val vRender = renderBackend ?: return
|
||||||
|
currentImageLoader = vRender.imageLoader
|
||||||
|
com.compose.sdl.text.currentViewportWidth = backend.pixelWidth
|
||||||
|
com.compose.sdl.text.currentViewportHeight = backend.pixelHeight
|
||||||
|
// Register bundled generic fonts (FontFamily.Monospace → NotoSansMono) once
|
||||||
|
// data.kres is loadable; idempotent, no-op if the font isn't bundled.
|
||||||
|
com.compose.sdl.text.registerGenericFonts()
|
||||||
|
// Position the OS IME candidate window the moment a field in THIS window
|
||||||
|
// gains focus, not only on the first TEXT_EDITING event (repointed per
|
||||||
|
// window here so multi-window focus targets the right SDL window).
|
||||||
|
com.compose.sdl.text.input.ImeBridge.onSessionActiveChange = { active ->
|
||||||
|
if (active) updateImeArea()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============
|
||||||
|
// Event handling (loop-routed, per window)
|
||||||
|
|
||||||
|
fun onPointerEvent(inEvent: AppEvent.Pointer) {
|
||||||
|
if (!facade.isEnabled) return
|
||||||
|
needsFrame = true
|
||||||
|
installGlobals()
|
||||||
|
val vType = when (inEvent.type) {
|
||||||
|
PointerEventType.Press -> 1
|
||||||
|
PointerEventType.Release -> 2
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
val vBtn = when (inEvent.button) {
|
||||||
|
PointerButton.Secondary -> 1
|
||||||
|
PointerButton.Tertiary -> 2
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
// SDL3 delivers mouse coords in logical points on HiDPI - multiply by
|
||||||
|
// DPR so hit-testing lands in the pixel space layout uses.
|
||||||
|
val vDpr = backend.pixelDensity
|
||||||
|
val vPx = inEvent.x * vDpr
|
||||||
|
val vPy = inEvent.y * vDpr
|
||||||
|
if (inEvent.type == PointerEventType.Press) {
|
||||||
|
popupHost?.notifyOutsidePress(vPx.toInt(), vPy.toInt())
|
||||||
|
}
|
||||||
|
host.onPointerRaw(vPx, vPy, vType, vBtn, SDL_GetTicks().toLong())
|
||||||
|
lastMouseX = vPx
|
||||||
|
lastMouseY = vPy
|
||||||
|
hasMousePos = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pointer left the window (SDL_EVENT_WINDOW_MOUSE_LEAVE). Stop the per-frame
|
||||||
|
synthetic hover re-dispatch and fire one Exit at the last position so a
|
||||||
|
hovered widget clears its highlight instead of sticking forever. */
|
||||||
|
fun onPointerExit() {
|
||||||
|
if (!facade.isEnabled) return
|
||||||
|
needsFrame = true
|
||||||
|
if (hasMousePos) {
|
||||||
|
host.onPointerRaw(lastMouseX, lastMouseY, 3, 0, SDL_GetTicks().toLong())
|
||||||
|
}
|
||||||
|
hasMousePos = false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onWheelEvent(inEvent: AppEvent.MouseWheel) {
|
||||||
|
if (!facade.isEnabled) return
|
||||||
|
needsFrame = true
|
||||||
|
installGlobals()
|
||||||
|
val vDpr = backend.pixelDensity
|
||||||
|
host.onWheel(inEvent.x * vDpr, inEvent.y * vDpr, inEvent.deltaX, inEvent.deltaY, SDL_GetTicks().toLong())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onKeyEvent(inEvent: AppEvent.Key) {
|
||||||
|
if (!facade.isEnabled) return
|
||||||
|
needsFrame = true
|
||||||
|
installGlobals()
|
||||||
|
// Compose Desktop's order: window preview → focused content → window
|
||||||
|
// onKeyEvent → app shortcut handler → back navigation (Escape).
|
||||||
|
if (previewKeyHandler().invoke(inEvent.event)) return
|
||||||
|
if (host.dispatchKeyEvent(inEvent.event)) return
|
||||||
|
if (keyHandler().invoke(inEvent.event)) return
|
||||||
|
if (!facade.dispatchKeyShortcut(inEvent.event)) {
|
||||||
|
backNavigationInput.onKeyEvent(inEvent.event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Applies the mutable window attributes. Called at init from the creation
|
||||||
|
flags and again whenever Window() sees one of them change. */
|
||||||
|
fun applyAttributes(inAttrs: WindowAttributes) {
|
||||||
|
facade.setUndecorated(inAttrs.undecorated)
|
||||||
|
facade.setResizable(inAttrs.resizable)
|
||||||
|
facade.setAlwaysOnTop(inAttrs.alwaysOnTop)
|
||||||
|
facade.setFocusable(inAttrs.focusable)
|
||||||
|
facade.setEnabled(inAttrs.enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onTextInputEvent(inEvent: AppEvent.TextInput) {
|
||||||
|
if (!facade.isEnabled) return
|
||||||
|
needsFrame = true
|
||||||
|
installGlobals()
|
||||||
|
// A focused text field runs an IME session (ComposeOwner.textInputSession):
|
||||||
|
// commit through it so the text REPLACES any active composition. Falls back
|
||||||
|
// to synthesising typed KeyEvents when no field is focused - SDL key events
|
||||||
|
// carry UNSHIFTED keycodes (no uppercase/numpad/dead keys), so committed
|
||||||
|
// text is the only layout-correct character source.
|
||||||
|
if (!com.compose.sdl.text.input.ImeBridge.commit(inEvent.text)) {
|
||||||
|
dispatchTypedText(host, inEvent.text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onTextEditingEvent(inEvent: AppEvent.TextEditing) {
|
||||||
|
if (!facade.isEnabled) return
|
||||||
|
needsFrame = true
|
||||||
|
installGlobals()
|
||||||
|
// IME preedit: show the composing (underlined) region in the focused field.
|
||||||
|
// No-op if no field is focused. Also nudge the OS candidate window to the
|
||||||
|
// field's on-screen rect.
|
||||||
|
com.compose.sdl.text.input.ImeBridge.compose(inEvent.text)
|
||||||
|
updateImeArea()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nudge the OS IME candidate window to the focused field's on-screen rect.
|
||||||
|
// Best-effort: no-op without an active session or a laid-out field.
|
||||||
|
@OptIn(androidx.compose.ui.ExperimentalComposeUiApi::class, kotlinx.cinterop.ExperimentalForeignApi::class)
|
||||||
|
private fun updateImeArea() {
|
||||||
|
val vRequest = com.compose.sdl.text.input.ImeBridge.request ?: return
|
||||||
|
val vRect = vRequest.focusedRectInRoot() ?: return
|
||||||
|
val vWindow = backend.window ?: return
|
||||||
|
val vDpr = backend.pixelDensity
|
||||||
|
kotlinx.cinterop.memScoped {
|
||||||
|
// SDL wants the area in logical window points; layout runs in physical px.
|
||||||
|
val vSdlRect = alloc<sdl3.SDL_Rect>()
|
||||||
|
vSdlRect.x = (vRect.left / vDpr).toInt()
|
||||||
|
vSdlRect.y = (vRect.top / vDpr).toInt()
|
||||||
|
vSdlRect.w = (vRect.width / vDpr).toInt().coerceAtLeast(1)
|
||||||
|
vSdlRect.h = (vRect.height / vDpr).toInt().coerceAtLeast(1)
|
||||||
|
sdl3.SDL_SetTextInputArea(vWindow.reinterpret(), vSdlRect.ptr, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onDropEvent(inEvent: AppEvent.Drop) {
|
||||||
|
if (!facade.isEnabled) return
|
||||||
|
needsFrame = true
|
||||||
|
installGlobals()
|
||||||
|
// SDL fires drop coords in logical points at DPR-1; scale to the pixel
|
||||||
|
// space layout runs in (Option-B density flow) so hit-testing lands
|
||||||
|
// where the pointer is.
|
||||||
|
val vDpr = backend.pixelDensity
|
||||||
|
when (inEvent.phase) {
|
||||||
|
AppEvent.DropPhase.BEGIN -> host.onDropBegin()
|
||||||
|
AppEvent.DropPhase.POSITION -> host.onDropPosition(inEvent.x * vDpr, inEvent.y * vDpr)
|
||||||
|
AppEvent.DropPhase.FILE -> inEvent.data?.let { host.onDropFile(it) }
|
||||||
|
AppEvent.DropPhase.TEXT -> inEvent.data?.let { host.onDropText(it) }
|
||||||
|
AppEvent.DropPhase.COMPLETE -> host.onDropComplete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onResizedEvent() {
|
||||||
|
needsFrame = true
|
||||||
|
backend.updateWindowSize()
|
||||||
|
facade.onResized()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** OS light/dark theme changed - re-pick the theme-appropriate window icon
|
||||||
|
(no-op if this window has no icon configured or the choice is unchanged). */
|
||||||
|
fun onSystemThemeChanged() {
|
||||||
|
backend.applyThemeIcon()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** OS close button / app-level Quit: honour the veto handler, then hand the
|
||||||
|
decision to the Window() declaration. */
|
||||||
|
fun requestClose() {
|
||||||
|
if (closeDispatched) return
|
||||||
|
if (facade.requestCloseFromUser()) {
|
||||||
|
closeDispatched = true
|
||||||
|
onCloseRequest()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============
|
||||||
|
// Frame pump
|
||||||
|
|
||||||
|
fun shouldRender(): Boolean =
|
||||||
|
needsFrame || (recomposer?.hasPendingWork == true) || onFrame != null || kForceRender
|
||||||
|
|
||||||
|
/** True while this window still has pending work: a state/layout/draw invalidation
|
||||||
|
(needsFrame), recomposer work, or a composition/node animation awaiting the next
|
||||||
|
frame. Sampled by windowHasInvalidations() from onFrame probes - the quiescence
|
||||||
|
signal for render-to-settle screenshot capture. */
|
||||||
|
fun hasInvalidations(): Boolean =
|
||||||
|
needsFrame || recomposer?.hasPendingWork == true ||
|
||||||
|
frameClock.hasAwaiters || host.hasAnimationAwaiters()
|
||||||
|
|
||||||
|
fun renderFrame() {
|
||||||
|
val vRender = renderBackend ?: return
|
||||||
|
needsFrame = false
|
||||||
|
if (recomposer?.hasPendingWork == true) needsFrame = true
|
||||||
|
|
||||||
|
backend.updateWindowSize()
|
||||||
|
vRender.ensureSize(backend.pixelWidth, backend.pixelHeight)
|
||||||
|
// Charge the GPU back-buffer acquire to its own phase. On Metal this is
|
||||||
|
// where nextDrawable() blocks on vsync (the natural frame pacing), so
|
||||||
|
// folding it into "layout" made the profiler read the vsync wait as
|
||||||
|
// layout cost - a ~6-7ms phantom that hid the real (tiny) layout time.
|
||||||
|
FrameProfiler.phase(" acquire")
|
||||||
|
host.setConstraints(backend.pixelWidth, backend.pixelHeight)
|
||||||
|
// Deliver any state written by this iteration's frame-clock continuations
|
||||||
|
// (withFrameNanos animations - notably smooth wheel scrolling) BEFORE we lay
|
||||||
|
// out. Those writes happen in the per-window pump's sendFrame()/yield(), which
|
||||||
|
// is AFTER the loop's last sendApplyNotifications() - so without this the
|
||||||
|
// relayout they trigger wasn't registered until the next frame, and the frame
|
||||||
|
// drew last frame's positions. That one-frame trail read as "the clip is a
|
||||||
|
// frame late" when scrolling (content briefly drawn past the viewport edge).
|
||||||
|
Snapshot.sendApplyNotifications()
|
||||||
|
host.measureAndLayout()
|
||||||
|
FrameProfiler.phase(" layout")
|
||||||
|
|
||||||
|
// Hover refresh after layout (upstream skiko: SyntheticEventSender).
|
||||||
|
if (hasMousePos) {
|
||||||
|
host.onPointerRaw(lastMouseX, lastMouseY, 0, 0, SDL_GetTicks().toLong())
|
||||||
|
}
|
||||||
|
|
||||||
|
vRender.beginFrame(1f)
|
||||||
|
vRender.drawRoot { canvas -> host.drawRoot(canvas) }
|
||||||
|
if (onFrame != null) {
|
||||||
|
renderingWindow = this
|
||||||
|
val vContinue = onFrame.invoke(vRender, frameIndex)
|
||||||
|
renderingWindow = null
|
||||||
|
// Probe consumers end the app when their scenario completes.
|
||||||
|
if (!vContinue) facade.close()
|
||||||
|
}
|
||||||
|
FrameProfiler.phase(" draw")
|
||||||
|
vRender.endFrame()
|
||||||
|
FrameProfiler.phase(" present")
|
||||||
|
frameIndex++
|
||||||
|
|
||||||
|
// FPS - instantaneous inter-frame rate, EMA-smoothed, title refreshed
|
||||||
|
// ~4x/sec. This shows within ~2 rendered frames of ANY activity rather
|
||||||
|
// than waiting for a full second of unbroken rendering to accumulate
|
||||||
|
// (the old fixed-window counter never got its first update: bursty
|
||||||
|
// interaction kept idling before 1s elapsed, and the idle-skip reset the
|
||||||
|
// window each time - so FPS only appeared during a long enough page
|
||||||
|
// transition). A dt outside 1..100ms is the first frame or a resume from
|
||||||
|
// idle (the gap isn't a real frame interval), so it's not sampled - the
|
||||||
|
// title just holds the last active rate while idle.
|
||||||
|
val vNowMs = SDL_GetTicks()
|
||||||
|
val vDt = (vNowMs - fpsLastFrameMs).toInt()
|
||||||
|
fpsLastFrameMs = vNowMs
|
||||||
|
if (vDt in 1..100) {
|
||||||
|
val vInst = 1000.0 / vDt
|
||||||
|
fpsEma = if (fpsEma <= 0.0) vInst else fpsEma * 0.9 + vInst * 0.1
|
||||||
|
if ((vNowMs - fpsLastTitleMs).toInt() >= 250) {
|
||||||
|
val vFps = (fpsEma + 0.5).toInt()
|
||||||
|
facade.updateFps(vFps)
|
||||||
|
SDL_SetWindowTitle(backend.window?.reinterpret(), "${facade.title} · $vFps FPS")
|
||||||
|
fpsLastTitleMs = vNowMs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Idle-skip calls this: drop the frame timer so the resume-from-idle frame's
|
||||||
|
// dt (the whole idle gap) isn't sampled as a real interval. The EMA is left
|
||||||
|
// intact so the title keeps showing the last active rate while idle.
|
||||||
|
fun resetFpsWindow() {
|
||||||
|
fpsLastFrameMs = SDL_GetTicks()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun destroy() {
|
||||||
|
composition?.dispose()
|
||||||
|
composition = null
|
||||||
|
// Window gone → DESTROYED lifecycle + ViewModels cleared (onCleared
|
||||||
|
// runs), matching upstream desktop's window-scoped owner lifetime.
|
||||||
|
architectureOwner.destroy()
|
||||||
|
recomposer?.cancel()
|
||||||
|
recomposer = null
|
||||||
|
recomposeJob?.cancel()
|
||||||
|
recomposeJob = null
|
||||||
|
renderBackend?.destroy()
|
||||||
|
renderBackend = null
|
||||||
|
backend.destroy(inQuitSdl = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2090,13 +2090,6 @@ final object androidx.compose.foundation/MarqueeDefaults { // androidx.compose.f
|
|||||||
final fun <get-Velocity>(): androidx.compose.ui.unit/Dp // androidx.compose.foundation/MarqueeDefaults.Velocity.<get-Velocity>|<get-Velocity>(){}[0]
|
final fun <get-Velocity>(): androidx.compose.ui.unit/Dp // androidx.compose.foundation/MarqueeDefaults.Velocity.<get-Velocity>|<get-Velocity>(){}[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
final object com.compose.sdl.icons/IconDefaults { // com.compose.sdl.icons/IconDefaults|null[0]
|
|
||||||
final val DefaultIconSize // com.compose.sdl.icons/IconDefaults.DefaultIconSize|{}DefaultIconSize[0]
|
|
||||||
final fun <get-DefaultIconSize>(): androidx.compose.ui.unit/Dp // com.compose.sdl.icons/IconDefaults.DefaultIconSize.<get-DefaultIconSize>|<get-DefaultIconSize>(){}[0]
|
|
||||||
final val LocalContentColor // com.compose.sdl.icons/IconDefaults.LocalContentColor|{}LocalContentColor[0]
|
|
||||||
final fun <get-LocalContentColor>(): androidx.compose.ui.graphics/Color // com.compose.sdl.icons/IconDefaults.LocalContentColor.<get-LocalContentColor>|<get-LocalContentColor>(){}[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
final object com.compose.sdl.widgets/SplitPaneDefaults { // com.compose.sdl.widgets/SplitPaneDefaults|null[0]
|
final object com.compose.sdl.widgets/SplitPaneDefaults { // com.compose.sdl.widgets/SplitPaneDefaults|null[0]
|
||||||
final val DividerThickness // com.compose.sdl.widgets/SplitPaneDefaults.DividerThickness|{}DividerThickness[0]
|
final val DividerThickness // com.compose.sdl.widgets/SplitPaneDefaults.DividerThickness|{}DividerThickness[0]
|
||||||
final fun <get-DividerThickness>(): androidx.compose.ui.unit/Dp // com.compose.sdl.widgets/SplitPaneDefaults.DividerThickness.<get-DividerThickness>|<get-DividerThickness>(){}[0]
|
final fun <get-DividerThickness>(): androidx.compose.ui.unit/Dp // com.compose.sdl.widgets/SplitPaneDefaults.DividerThickness.<get-DividerThickness>|<get-DividerThickness>(){}[0]
|
||||||
@@ -2233,7 +2226,6 @@ final val androidx.compose.foundation/androidx_compose_foundation_MarqueeDefault
|
|||||||
final val androidx.compose.foundation/androidx_compose_foundation_MutatorMutex$stableprop // androidx.compose.foundation/androidx_compose_foundation_MutatorMutex$stableprop|#static{}androidx_compose_foundation_MutatorMutex$stableprop[0]
|
final val androidx.compose.foundation/androidx_compose_foundation_MutatorMutex$stableprop // androidx.compose.foundation/androidx_compose_foundation_MutatorMutex$stableprop|#static{}androidx_compose_foundation_MutatorMutex$stableprop[0]
|
||||||
final val androidx.compose.foundation/androidx_compose_foundation_ScrollState$stableprop // androidx.compose.foundation/androidx_compose_foundation_ScrollState$stableprop|#static{}androidx_compose_foundation_ScrollState$stableprop[0]
|
final val androidx.compose.foundation/androidx_compose_foundation_ScrollState$stableprop // androidx.compose.foundation/androidx_compose_foundation_ScrollState$stableprop|#static{}androidx_compose_foundation_ScrollState$stableprop[0]
|
||||||
final val androidx.compose.foundation/androidx_compose_foundation_ScrollbarStyle$stableprop // androidx.compose.foundation/androidx_compose_foundation_ScrollbarStyle$stableprop|#static{}androidx_compose_foundation_ScrollbarStyle$stableprop[0]
|
final val androidx.compose.foundation/androidx_compose_foundation_ScrollbarStyle$stableprop // androidx.compose.foundation/androidx_compose_foundation_ScrollbarStyle$stableprop|#static{}androidx_compose_foundation_ScrollbarStyle$stableprop[0]
|
||||||
final val com.compose.sdl.icons/com_compose_sdl_icons_IconDefaults$stableprop // com.compose.sdl.icons/com_compose_sdl_icons_IconDefaults$stableprop|#static{}com_compose_sdl_icons_IconDefaults$stableprop[0]
|
|
||||||
final val com.compose.sdl.widgets/com_compose_sdl_widgets_SplitPaneDefaults$stableprop // com.compose.sdl.widgets/com_compose_sdl_widgets_SplitPaneDefaults$stableprop|#static{}com_compose_sdl_widgets_SplitPaneDefaults$stableprop[0]
|
final val com.compose.sdl.widgets/com_compose_sdl_widgets_SplitPaneDefaults$stableprop // com.compose.sdl.widgets/com_compose_sdl_widgets_SplitPaneDefaults$stableprop|#static{}com_compose_sdl_widgets_SplitPaneDefaults$stableprop[0]
|
||||||
|
|
||||||
final fun (androidx.compose.foundation.content/TransferableContent).androidx.compose.foundation.content/hasMediaType(androidx.compose.foundation.content/MediaType): kotlin/Boolean // androidx.compose.foundation.content/hasMediaType|hasMediaType@androidx.compose.foundation.content.TransferableContent(androidx.compose.foundation.content.MediaType){}[0]
|
final fun (androidx.compose.foundation.content/TransferableContent).androidx.compose.foundation.content/hasMediaType(androidx.compose.foundation.content/MediaType): kotlin/Boolean // androidx.compose.foundation.content/hasMediaType|hasMediaType@androidx.compose.foundation.content.TransferableContent(androidx.compose.foundation.content.MediaType){}[0]
|
||||||
@@ -2651,7 +2643,6 @@ final fun androidx.compose.foundation/rememberScrollbarAdapter(androidx.compose.
|
|||||||
final fun androidx.compose.foundation/rememberScrollbarAdapter(androidx.compose.foundation.text/TextFieldScrollState, androidx.compose.runtime/Composer?, kotlin/Int): androidx.compose.foundation.v2/ScrollbarAdapter // androidx.compose.foundation/rememberScrollbarAdapter|rememberScrollbarAdapter(androidx.compose.foundation.text.TextFieldScrollState;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
|
final fun androidx.compose.foundation/rememberScrollbarAdapter(androidx.compose.foundation.text/TextFieldScrollState, androidx.compose.runtime/Composer?, kotlin/Int): androidx.compose.foundation.v2/ScrollbarAdapter // androidx.compose.foundation/rememberScrollbarAdapter|rememberScrollbarAdapter(androidx.compose.foundation.text.TextFieldScrollState;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
|
||||||
final fun androidx.compose.foundation/rememberScrollbarAdapter(androidx.compose.foundation/ScrollState, androidx.compose.runtime/Composer?, kotlin/Int): androidx.compose.foundation.v2/ScrollbarAdapter // androidx.compose.foundation/rememberScrollbarAdapter|rememberScrollbarAdapter(androidx.compose.foundation.ScrollState;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
|
final fun androidx.compose.foundation/rememberScrollbarAdapter(androidx.compose.foundation/ScrollState, androidx.compose.runtime/Composer?, kotlin/Int): androidx.compose.foundation.v2/ScrollbarAdapter // androidx.compose.foundation/rememberScrollbarAdapter|rememberScrollbarAdapter(androidx.compose.foundation.ScrollState;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
|
||||||
final fun com.compose.sdl.icons/IconFontIcon(kotlin/Int, kotlin/String, kotlin/String?, androidx.compose.ui/Modifier?, androidx.compose.ui.graphics/Color, androidx.compose.ui.unit/Dp, kotlin.collections/List<androidx.compose.ui.text.font/FontVariation.Setting>?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.compose.sdl.icons/IconFontIcon|IconFontIcon(kotlin.Int;kotlin.String;kotlin.String?;androidx.compose.ui.Modifier?;androidx.compose.ui.graphics.Color;androidx.compose.ui.unit.Dp;kotlin.collections.List<androidx.compose.ui.text.font.FontVariation.Setting>?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
final fun com.compose.sdl.icons/IconFontIcon(kotlin/Int, kotlin/String, kotlin/String?, androidx.compose.ui/Modifier?, androidx.compose.ui.graphics/Color, androidx.compose.ui.unit/Dp, kotlin.collections/List<androidx.compose.ui.text.font/FontVariation.Setting>?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.compose.sdl.icons/IconFontIcon|IconFontIcon(kotlin.Int;kotlin.String;kotlin.String?;androidx.compose.ui.Modifier?;androidx.compose.ui.graphics.Color;androidx.compose.ui.unit.Dp;kotlin.collections.List<androidx.compose.ui.text.font.FontVariation.Setting>?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
||||||
final fun com.compose.sdl.icons/com_compose_sdl_icons_IconDefaults$stableprop_getter(): kotlin/Int // com.compose.sdl.icons/com_compose_sdl_icons_IconDefaults$stableprop_getter|com_compose_sdl_icons_IconDefaults$stableprop_getter(){}[0]
|
|
||||||
final fun com.compose.sdl.modifier/rememberMutableInteractionSource(androidx.compose.runtime/Composer?, kotlin/Int): androidx.compose.foundation.interaction/MutableInteractionSource // com.compose.sdl.modifier/rememberMutableInteractionSource|rememberMutableInteractionSource(androidx.compose.runtime.Composer?;kotlin.Int){}[0]
|
final fun com.compose.sdl.modifier/rememberMutableInteractionSource(androidx.compose.runtime/Composer?, kotlin/Int): androidx.compose.foundation.interaction/MutableInteractionSource // com.compose.sdl.modifier/rememberMutableInteractionSource|rememberMutableInteractionSource(androidx.compose.runtime.Composer?;kotlin.Int){}[0]
|
||||||
final fun com.compose.sdl.text/IconText(kotlin/String, kotlin/String, androidx.compose.ui/Modifier?, androidx.compose.ui.graphics/Color, androidx.compose.ui.unit/TextUnit, androidx.compose.ui.text.style/TextAlign, kotlin.collections/List<androidx.compose.ui.text.font/FontVariation.Setting>?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.compose.sdl.text/IconText|IconText(kotlin.String;kotlin.String;androidx.compose.ui.Modifier?;androidx.compose.ui.graphics.Color;androidx.compose.ui.unit.TextUnit;androidx.compose.ui.text.style.TextAlign;kotlin.collections.List<androidx.compose.ui.text.font.FontVariation.Setting>?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
final fun com.compose.sdl.text/IconText(kotlin/String, kotlin/String, androidx.compose.ui/Modifier?, androidx.compose.ui.graphics/Color, androidx.compose.ui.unit/TextUnit, androidx.compose.ui.text.style/TextAlign, kotlin.collections/List<androidx.compose.ui.text.font/FontVariation.Setting>?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.compose.sdl.text/IconText|IconText(kotlin.String;kotlin.String;androidx.compose.ui.Modifier?;androidx.compose.ui.graphics.Color;androidx.compose.ui.unit.TextUnit;androidx.compose.ui.text.style.TextAlign;kotlin.collections.List<androidx.compose.ui.text.font.FontVariation.Setting>?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
||||||
final fun com.compose.sdl.text/IconText(kotlin/String, kotlin/String, androidx.compose.ui/Modifier?, androidx.compose.ui.graphics/Color, androidx.compose.ui.unit/TextUnit, androidx.compose.ui.text.style/TextAlign?, kotlin.collections/List<androidx.compose.ui.text.font/FontVariation.Setting>?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.compose.sdl.text/IconText|IconText(kotlin.String;kotlin.String;androidx.compose.ui.Modifier?;androidx.compose.ui.graphics.Color;androidx.compose.ui.unit.TextUnit;androidx.compose.ui.text.style.TextAlign?;kotlin.collections.List<androidx.compose.ui.text.font.FontVariation.Setting>?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
final fun com.compose.sdl.text/IconText(kotlin/String, kotlin/String, androidx.compose.ui/Modifier?, androidx.compose.ui.graphics/Color, androidx.compose.ui.unit/TextUnit, androidx.compose.ui.text.style/TextAlign?, kotlin.collections/List<androidx.compose.ui.text.font/FontVariation.Setting>?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // com.compose.sdl.text/IconText|IconText(kotlin.String;kotlin.String;androidx.compose.ui.Modifier?;androidx.compose.ui.graphics.Color;androidx.compose.ui.unit.TextUnit;androidx.compose.ui.text.style.TextAlign?;kotlin.collections.List<androidx.compose.ui.text.font.FontVariation.Setting>?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
|
||||||
|
|||||||
+1
-1
@@ -61,7 +61,7 @@ fun IconFontIcon(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object IconDefaults {
|
internal object IconDefaults {
|
||||||
val DefaultIconSize: Dp = 24.dp
|
val DefaultIconSize: Dp = 24.dp
|
||||||
/** STATIC fallback tint for direct IconFontIcon use - :foundation cannot
|
/** STATIC fallback tint for direct IconFontIcon use - :foundation cannot
|
||||||
read material3's LocalContentColor (module layering, same as upstream).
|
read material3's LocalContentColor (module layering, same as upstream).
|
||||||
|
|||||||
@@ -341,18 +341,10 @@ abstract interface com.compose.sdl.graphics/NativeFinishableCanvas { // com.comp
|
|||||||
abstract fun finish() // com.compose.sdl.graphics/NativeFinishableCanvas.finish|finish(){}[0]
|
abstract fun finish() // com.compose.sdl.graphics/NativeFinishableCanvas.finish|finish(){}[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract interface com.compose.sdl.graphics/NativePainterCanvas { // com.compose.sdl.graphics/NativePainterCanvas|null[0]
|
|
||||||
abstract fun drawNativePainter(kotlin/String, com.compose.sdl.res/ResourceKind, kotlin/Float, kotlin/Float, kotlin/Float, kotlin/Float, kotlin/Float) // com.compose.sdl.graphics/NativePainterCanvas.drawNativePainter|drawNativePainter(kotlin.String;com.compose.sdl.res.ResourceKind;kotlin.Float;kotlin.Float;kotlin.Float;kotlin.Float;kotlin.Float){}[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract interface com.compose.sdl.graphics/NativeShadowCanvas { // com.compose.sdl.graphics/NativeShadowCanvas|null[0]
|
abstract interface com.compose.sdl.graphics/NativeShadowCanvas { // com.compose.sdl.graphics/NativeShadowCanvas|null[0]
|
||||||
abstract fun drawDropShadow(androidx.compose.ui.graphics/Outline, kotlin/Float, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color) // com.compose.sdl.graphics/NativeShadowCanvas.drawDropShadow|drawDropShadow(androidx.compose.ui.graphics.Outline;kotlin.Float;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color){}[0]
|
abstract fun drawDropShadow(androidx.compose.ui.graphics/Outline, kotlin/Float, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color) // com.compose.sdl.graphics/NativeShadowCanvas.drawDropShadow|drawDropShadow(androidx.compose.ui.graphics.Outline;kotlin.Float;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color){}[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract interface com.compose.sdl.graphics/NativeShapeClipCanvas { // com.compose.sdl.graphics/NativeShapeClipCanvas|null[0]
|
|
||||||
abstract fun clipRoundRect(androidx.compose.ui.geometry/RoundRect) // com.compose.sdl.graphics/NativeShapeClipCanvas.clipRoundRect|clipRoundRect(androidx.compose.ui.geometry.RoundRect){}[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract interface com.compose.sdl.res/ImageLoader { // com.compose.sdl.res/ImageLoader|null[0]
|
abstract interface com.compose.sdl.res/ImageLoader { // com.compose.sdl.res/ImageLoader|null[0]
|
||||||
abstract fun intrinsicSize(kotlin/String, com.compose.sdl.res/ResourceKind): androidx.compose.ui.geometry/Size // com.compose.sdl.res/ImageLoader.intrinsicSize|intrinsicSize(kotlin.String;com.compose.sdl.res.ResourceKind){}[0]
|
abstract fun intrinsicSize(kotlin/String, com.compose.sdl.res/ResourceKind): androidx.compose.ui.geometry/Size // com.compose.sdl.res/ImageLoader.intrinsicSize|intrinsicSize(kotlin.String;com.compose.sdl.res.ResourceKind){}[0]
|
||||||
abstract fun readBytes(kotlin/String): kotlin/ByteArray? // com.compose.sdl.res/ImageLoader.readBytes|readBytes(kotlin.String){}[0]
|
abstract fun readBytes(kotlin/String): kotlin/ByteArray? // com.compose.sdl.res/ImageLoader.readBytes|readBytes(kotlin.String){}[0]
|
||||||
@@ -364,62 +356,6 @@ sealed interface androidx.compose.ui.graphics.shadow/ShadowContext { // androidx
|
|||||||
open fun createInnerShadowPainter(androidx.compose.ui.graphics/Shape, androidx.compose.ui.graphics.shadow/Shadow): androidx.compose.ui.graphics.shadow/InnerShadowPainter // androidx.compose.ui.graphics.shadow/ShadowContext.createInnerShadowPainter|createInnerShadowPainter(androidx.compose.ui.graphics.Shape;androidx.compose.ui.graphics.shadow.Shadow){}[0]
|
open fun createInnerShadowPainter(androidx.compose.ui.graphics/Shape, androidx.compose.ui.graphics.shadow/Shadow): androidx.compose.ui.graphics.shadow/InnerShadowPainter // androidx.compose.ui.graphics.shadow/ShadowContext.createInnerShadowPainter|createInnerShadowPainter(androidx.compose.ui.graphics.Shape;androidx.compose.ui.graphics.shadow.Shadow){}[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface com.compose.sdl.graphics/PathCommand { // com.compose.sdl.graphics/PathCommand|null[0]
|
|
||||||
final class CubicTo : com.compose.sdl.graphics/PathCommand { // com.compose.sdl.graphics/PathCommand.CubicTo|null[0]
|
|
||||||
constructor <init>(kotlin/Float, kotlin/Float, kotlin/Float, kotlin/Float, kotlin/Float, kotlin/Float) // com.compose.sdl.graphics/PathCommand.CubicTo.<init>|<init>(kotlin.Float;kotlin.Float;kotlin.Float;kotlin.Float;kotlin.Float;kotlin.Float){}[0]
|
|
||||||
|
|
||||||
final val c1x // com.compose.sdl.graphics/PathCommand.CubicTo.c1x|{}c1x[0]
|
|
||||||
final fun <get-c1x>(): kotlin/Float // com.compose.sdl.graphics/PathCommand.CubicTo.c1x.<get-c1x>|<get-c1x>(){}[0]
|
|
||||||
final val c1y // com.compose.sdl.graphics/PathCommand.CubicTo.c1y|{}c1y[0]
|
|
||||||
final fun <get-c1y>(): kotlin/Float // com.compose.sdl.graphics/PathCommand.CubicTo.c1y.<get-c1y>|<get-c1y>(){}[0]
|
|
||||||
final val c2x // com.compose.sdl.graphics/PathCommand.CubicTo.c2x|{}c2x[0]
|
|
||||||
final fun <get-c2x>(): kotlin/Float // com.compose.sdl.graphics/PathCommand.CubicTo.c2x.<get-c2x>|<get-c2x>(){}[0]
|
|
||||||
final val c2y // com.compose.sdl.graphics/PathCommand.CubicTo.c2y|{}c2y[0]
|
|
||||||
final fun <get-c2y>(): kotlin/Float // com.compose.sdl.graphics/PathCommand.CubicTo.c2y.<get-c2y>|<get-c2y>(){}[0]
|
|
||||||
final val x // com.compose.sdl.graphics/PathCommand.CubicTo.x|{}x[0]
|
|
||||||
final fun <get-x>(): kotlin/Float // com.compose.sdl.graphics/PathCommand.CubicTo.x.<get-x>|<get-x>(){}[0]
|
|
||||||
final val y // com.compose.sdl.graphics/PathCommand.CubicTo.y|{}y[0]
|
|
||||||
final fun <get-y>(): kotlin/Float // com.compose.sdl.graphics/PathCommand.CubicTo.y.<get-y>|<get-y>(){}[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
final class LineTo : com.compose.sdl.graphics/PathCommand { // com.compose.sdl.graphics/PathCommand.LineTo|null[0]
|
|
||||||
constructor <init>(kotlin/Float, kotlin/Float) // com.compose.sdl.graphics/PathCommand.LineTo.<init>|<init>(kotlin.Float;kotlin.Float){}[0]
|
|
||||||
|
|
||||||
final val x // com.compose.sdl.graphics/PathCommand.LineTo.x|{}x[0]
|
|
||||||
final fun <get-x>(): kotlin/Float // com.compose.sdl.graphics/PathCommand.LineTo.x.<get-x>|<get-x>(){}[0]
|
|
||||||
final val y // com.compose.sdl.graphics/PathCommand.LineTo.y|{}y[0]
|
|
||||||
final fun <get-y>(): kotlin/Float // com.compose.sdl.graphics/PathCommand.LineTo.y.<get-y>|<get-y>(){}[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
final class MoveTo : com.compose.sdl.graphics/PathCommand { // com.compose.sdl.graphics/PathCommand.MoveTo|null[0]
|
|
||||||
constructor <init>(kotlin/Float, kotlin/Float) // com.compose.sdl.graphics/PathCommand.MoveTo.<init>|<init>(kotlin.Float;kotlin.Float){}[0]
|
|
||||||
|
|
||||||
final val x // com.compose.sdl.graphics/PathCommand.MoveTo.x|{}x[0]
|
|
||||||
final fun <get-x>(): kotlin/Float // com.compose.sdl.graphics/PathCommand.MoveTo.x.<get-x>|<get-x>(){}[0]
|
|
||||||
final val y // com.compose.sdl.graphics/PathCommand.MoveTo.y|{}y[0]
|
|
||||||
final fun <get-y>(): kotlin/Float // com.compose.sdl.graphics/PathCommand.MoveTo.y.<get-y>|<get-y>(){}[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
final class QuadTo : com.compose.sdl.graphics/PathCommand { // com.compose.sdl.graphics/PathCommand.QuadTo|null[0]
|
|
||||||
constructor <init>(kotlin/Float, kotlin/Float, kotlin/Float, kotlin/Float) // com.compose.sdl.graphics/PathCommand.QuadTo.<init>|<init>(kotlin.Float;kotlin.Float;kotlin.Float;kotlin.Float){}[0]
|
|
||||||
|
|
||||||
final val cx // com.compose.sdl.graphics/PathCommand.QuadTo.cx|{}cx[0]
|
|
||||||
final fun <get-cx>(): kotlin/Float // com.compose.sdl.graphics/PathCommand.QuadTo.cx.<get-cx>|<get-cx>(){}[0]
|
|
||||||
final val cy // com.compose.sdl.graphics/PathCommand.QuadTo.cy|{}cy[0]
|
|
||||||
final fun <get-cy>(): kotlin/Float // com.compose.sdl.graphics/PathCommand.QuadTo.cy.<get-cy>|<get-cy>(){}[0]
|
|
||||||
final val x // com.compose.sdl.graphics/PathCommand.QuadTo.x|{}x[0]
|
|
||||||
final fun <get-x>(): kotlin/Float // com.compose.sdl.graphics/PathCommand.QuadTo.x.<get-x>|<get-x>(){}[0]
|
|
||||||
final val y // com.compose.sdl.graphics/PathCommand.QuadTo.y|{}y[0]
|
|
||||||
final fun <get-y>(): kotlin/Float // com.compose.sdl.graphics/PathCommand.QuadTo.y.<get-y>|<get-y>(){}[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
final object Close : com.compose.sdl.graphics/PathCommand { // com.compose.sdl.graphics/PathCommand.Close|null[0]
|
|
||||||
final fun equals(kotlin/Any?): kotlin/Boolean // com.compose.sdl.graphics/PathCommand.Close.equals|equals(kotlin.Any?){}[0]
|
|
||||||
final fun hashCode(): kotlin/Int // com.compose.sdl.graphics/PathCommand.Close.hashCode|hashCode(){}[0]
|
|
||||||
final fun toString(): kotlin/String // com.compose.sdl.graphics/PathCommand.Close.toString|toString(){}[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class androidx.compose.ui.graphics.colorspace/Adaptation { // androidx.compose.ui.graphics.colorspace/Adaptation|null[0]
|
abstract class androidx.compose.ui.graphics.colorspace/Adaptation { // androidx.compose.ui.graphics.colorspace/Adaptation|null[0]
|
||||||
final object Companion { // androidx.compose.ui.graphics.colorspace/Adaptation.Companion|null[0]
|
final object Companion { // androidx.compose.ui.graphics.colorspace/Adaptation.Companion|null[0]
|
||||||
final val Bradford // androidx.compose.ui.graphics.colorspace/Adaptation.Companion.Bradford|{}Bradford[0]
|
final val Bradford // androidx.compose.ui.graphics.colorspace/Adaptation.Companion.Bradford|{}Bradford[0]
|
||||||
@@ -1111,19 +1047,6 @@ final class com.compose.sdl.renderer.skia/SkiaLeafDrawer { // com.compose.sdl.re
|
|||||||
final fun <get-imageCache>(): com.compose.sdl.renderer.skia/SkiaImageCache // com.compose.sdl.renderer.skia/SkiaLeafDrawer.imageCache.<get-imageCache>|<get-imageCache>(){}[0]
|
final fun <get-imageCache>(): com.compose.sdl.renderer.skia/SkiaImageCache // com.compose.sdl.renderer.skia/SkiaLeafDrawer.imageCache.<get-imageCache>|<get-imageCache>(){}[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
final class com.compose.sdl.res/ResourcePainter : androidx.compose.ui.graphics.painter/Painter { // com.compose.sdl.res/ResourcePainter|null[0]
|
|
||||||
final val intrinsicSize // com.compose.sdl.res/ResourcePainter.intrinsicSize|{}intrinsicSize[0]
|
|
||||||
final fun <get-intrinsicSize>(): androidx.compose.ui.geometry/Size // com.compose.sdl.res/ResourcePainter.intrinsicSize.<get-intrinsicSize>|<get-intrinsicSize>(){}[0]
|
|
||||||
final val kind // com.compose.sdl.res/ResourcePainter.kind|{}kind[0]
|
|
||||||
final fun <get-kind>(): com.compose.sdl.res/ResourceKind // com.compose.sdl.res/ResourcePainter.kind.<get-kind>|<get-kind>(){}[0]
|
|
||||||
final val resourcePath // com.compose.sdl.res/ResourcePainter.resourcePath|{}resourcePath[0]
|
|
||||||
final fun <get-resourcePath>(): kotlin/String // com.compose.sdl.res/ResourcePainter.resourcePath.<get-resourcePath>|<get-resourcePath>(){}[0]
|
|
||||||
|
|
||||||
final fun equals(kotlin/Any?): kotlin/Boolean // com.compose.sdl.res/ResourcePainter.equals|equals(kotlin.Any?){}[0]
|
|
||||||
final fun hashCode(): kotlin/Int // com.compose.sdl.res/ResourcePainter.hashCode|hashCode(){}[0]
|
|
||||||
final fun toString(): kotlin/String // com.compose.sdl.res/ResourcePainter.toString|toString(){}[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
final value class androidx.compose.ui.graphics.colorspace/ColorModel { // androidx.compose.ui.graphics.colorspace/ColorModel|null[0]
|
final value class androidx.compose.ui.graphics.colorspace/ColorModel { // androidx.compose.ui.graphics.colorspace/ColorModel|null[0]
|
||||||
final val componentCount // androidx.compose.ui.graphics.colorspace/ColorModel.componentCount|{}componentCount[0]
|
final val componentCount // androidx.compose.ui.graphics.colorspace/ColorModel.componentCount|{}componentCount[0]
|
||||||
final fun <get-componentCount>(): kotlin/Int // androidx.compose.ui.graphics.colorspace/ColorModel.componentCount.<get-componentCount>|<get-componentCount>(){}[0]
|
final fun <get-componentCount>(): kotlin/Int // androidx.compose.ui.graphics.colorspace/ColorModel.componentCount.<get-componentCount>|<get-componentCount>(){}[0]
|
||||||
@@ -2108,10 +2031,6 @@ final object com.compose.sdl.graphics/NativeReleaseQueue { // com.compose.sdl.gr
|
|||||||
final fun enqueue(kotlin/Function0<kotlin/Unit>) // com.compose.sdl.graphics/NativeReleaseQueue.enqueue|enqueue(kotlin.Function0<kotlin.Unit>){}[0]
|
final fun enqueue(kotlin/Function0<kotlin/Unit>) // com.compose.sdl.graphics/NativeReleaseQueue.enqueue|enqueue(kotlin.Function0<kotlin.Unit>){}[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
final object com.compose.sdl.res/AndroidVectorToSvg { // com.compose.sdl.res/AndroidVectorToSvg|null[0]
|
|
||||||
final fun convert(kotlin/String): kotlin/String // com.compose.sdl.res/AndroidVectorToSvg.convert|convert(kotlin.String){}[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
final object com.compose.sdl.res/Res { // com.compose.sdl.res/Res|null[0]
|
final object com.compose.sdl.res/Res { // com.compose.sdl.res/Res|null[0]
|
||||||
final fun readBytes(kotlin/String): kotlin/ByteArray? // com.compose.sdl.res/Res.readBytes|readBytes(kotlin.String){}[0]
|
final fun readBytes(kotlin/String): kotlin/ByteArray? // com.compose.sdl.res/Res.readBytes|readBytes(kotlin.String){}[0]
|
||||||
|
|
||||||
@@ -2261,11 +2180,9 @@ final val com.compose.sdl.graphics/gradientTileMode // com.compose.sdl.graphics/
|
|||||||
final val com.compose.sdl.renderer.skia/com_compose_sdl_renderer_skia_SkiaEncodedImageDecoder$stableprop // com.compose.sdl.renderer.skia/com_compose_sdl_renderer_skia_SkiaEncodedImageDecoder$stableprop|#static{}com_compose_sdl_renderer_skia_SkiaEncodedImageDecoder$stableprop[0]
|
final val com.compose.sdl.renderer.skia/com_compose_sdl_renderer_skia_SkiaEncodedImageDecoder$stableprop // com.compose.sdl.renderer.skia/com_compose_sdl_renderer_skia_SkiaEncodedImageDecoder$stableprop|#static{}com_compose_sdl_renderer_skia_SkiaEncodedImageDecoder$stableprop[0]
|
||||||
final val com.compose.sdl.renderer.skia/com_compose_sdl_renderer_skia_SkiaImageCache$stableprop // com.compose.sdl.renderer.skia/com_compose_sdl_renderer_skia_SkiaImageCache$stableprop|#static{}com_compose_sdl_renderer_skia_SkiaImageCache$stableprop[0]
|
final val com.compose.sdl.renderer.skia/com_compose_sdl_renderer_skia_SkiaImageCache$stableprop // com.compose.sdl.renderer.skia/com_compose_sdl_renderer_skia_SkiaImageCache$stableprop|#static{}com_compose_sdl_renderer_skia_SkiaImageCache$stableprop[0]
|
||||||
final val com.compose.sdl.renderer.skia/com_compose_sdl_renderer_skia_SkiaLeafDrawer$stableprop // com.compose.sdl.renderer.skia/com_compose_sdl_renderer_skia_SkiaLeafDrawer$stableprop|#static{}com_compose_sdl_renderer_skia_SkiaLeafDrawer$stableprop[0]
|
final val com.compose.sdl.renderer.skia/com_compose_sdl_renderer_skia_SkiaLeafDrawer$stableprop // com.compose.sdl.renderer.skia/com_compose_sdl_renderer_skia_SkiaLeafDrawer$stableprop|#static{}com_compose_sdl_renderer_skia_SkiaLeafDrawer$stableprop[0]
|
||||||
final val com.compose.sdl.res/com_compose_sdl_res_AndroidVectorToSvg$stableprop // com.compose.sdl.res/com_compose_sdl_res_AndroidVectorToSvg$stableprop|#static{}com_compose_sdl_res_AndroidVectorToSvg$stableprop[0]
|
|
||||||
final val com.compose.sdl.res/com_compose_sdl_res_Res$stableprop // com.compose.sdl.res/com_compose_sdl_res_Res$stableprop|#static{}com_compose_sdl_res_Res$stableprop[0]
|
final val com.compose.sdl.res/com_compose_sdl_res_Res$stableprop // com.compose.sdl.res/com_compose_sdl_res_Res$stableprop|#static{}com_compose_sdl_res_Res$stableprop[0]
|
||||||
final val com.compose.sdl.res/com_compose_sdl_res_Res_drawable$stableprop // com.compose.sdl.res/com_compose_sdl_res_Res_drawable$stableprop|#static{}com_compose_sdl_res_Res_drawable$stableprop[0]
|
final val com.compose.sdl.res/com_compose_sdl_res_Res_drawable$stableprop // com.compose.sdl.res/com_compose_sdl_res_Res_drawable$stableprop|#static{}com_compose_sdl_res_Res_drawable$stableprop[0]
|
||||||
final val com.compose.sdl.res/com_compose_sdl_res_Res_files$stableprop // com.compose.sdl.res/com_compose_sdl_res_Res_files$stableprop|#static{}com_compose_sdl_res_Res_files$stableprop[0]
|
final val com.compose.sdl.res/com_compose_sdl_res_Res_files$stableprop // com.compose.sdl.res/com_compose_sdl_res_Res_files$stableprop|#static{}com_compose_sdl_res_Res_files$stableprop[0]
|
||||||
final val com.compose.sdl.res/com_compose_sdl_res_ResourcePainter$stableprop // com.compose.sdl.res/com_compose_sdl_res_ResourcePainter$stableprop|#static{}com_compose_sdl_res_ResourcePainter$stableprop[0]
|
|
||||||
|
|
||||||
final var androidx.compose.ui.graphics/alphaMultiplier // androidx.compose.ui.graphics/alphaMultiplier|@androidx.compose.ui.graphics.Canvas{}alphaMultiplier[0]
|
final var androidx.compose.ui.graphics/alphaMultiplier // androidx.compose.ui.graphics/alphaMultiplier|@androidx.compose.ui.graphics.Canvas{}alphaMultiplier[0]
|
||||||
final fun (androidx.compose.ui.graphics/Canvas).<get-alphaMultiplier>(): kotlin/Float // androidx.compose.ui.graphics/alphaMultiplier.<get-alphaMultiplier>|<get-alphaMultiplier>@androidx.compose.ui.graphics.Canvas(){}[0]
|
final fun (androidx.compose.ui.graphics/Canvas).<get-alphaMultiplier>(): kotlin/Float // androidx.compose.ui.graphics/alphaMultiplier.<get-alphaMultiplier>|<get-alphaMultiplier>@androidx.compose.ui.graphics.Canvas(){}[0]
|
||||||
@@ -2456,14 +2373,11 @@ final fun com.compose.sdl.renderer.skia/com_compose_sdl_renderer_skia_SkiaEncode
|
|||||||
final fun com.compose.sdl.renderer.skia/com_compose_sdl_renderer_skia_SkiaImageCache$stableprop_getter(): kotlin/Int // com.compose.sdl.renderer.skia/com_compose_sdl_renderer_skia_SkiaImageCache$stableprop_getter|com_compose_sdl_renderer_skia_SkiaImageCache$stableprop_getter(){}[0]
|
final fun com.compose.sdl.renderer.skia/com_compose_sdl_renderer_skia_SkiaImageCache$stableprop_getter(): kotlin/Int // com.compose.sdl.renderer.skia/com_compose_sdl_renderer_skia_SkiaImageCache$stableprop_getter|com_compose_sdl_renderer_skia_SkiaImageCache$stableprop_getter(){}[0]
|
||||||
final fun com.compose.sdl.renderer.skia/com_compose_sdl_renderer_skia_SkiaLeafDrawer$stableprop_getter(): kotlin/Int // com.compose.sdl.renderer.skia/com_compose_sdl_renderer_skia_SkiaLeafDrawer$stableprop_getter|com_compose_sdl_renderer_skia_SkiaLeafDrawer$stableprop_getter(){}[0]
|
final fun com.compose.sdl.renderer.skia/com_compose_sdl_renderer_skia_SkiaLeafDrawer$stableprop_getter(): kotlin/Int // com.compose.sdl.renderer.skia/com_compose_sdl_renderer_skia_SkiaLeafDrawer$stableprop_getter|com_compose_sdl_renderer_skia_SkiaLeafDrawer$stableprop_getter(){}[0]
|
||||||
final fun com.compose.sdl.renderer.skia/readBackBgra(org.jetbrains.skia/Surface?, kotlin/Int, kotlin/Int): kotlin/Triple<kotlin/Int, kotlin/Int, kotlin/ByteArray>? // com.compose.sdl.renderer.skia/readBackBgra|readBackBgra(org.jetbrains.skia.Surface?;kotlin.Int;kotlin.Int){}[0]
|
final fun com.compose.sdl.renderer.skia/readBackBgra(org.jetbrains.skia/Surface?, kotlin/Int, kotlin/Int): kotlin/Triple<kotlin/Int, kotlin/Int, kotlin/ByteArray>? // com.compose.sdl.renderer.skia/readBackBgra|readBackBgra(org.jetbrains.skia.Surface?;kotlin.Int;kotlin.Int){}[0]
|
||||||
final fun com.compose.sdl.res/com_compose_sdl_res_AndroidVectorToSvg$stableprop_getter(): kotlin/Int // com.compose.sdl.res/com_compose_sdl_res_AndroidVectorToSvg$stableprop_getter|com_compose_sdl_res_AndroidVectorToSvg$stableprop_getter(){}[0]
|
|
||||||
final fun com.compose.sdl.res/com_compose_sdl_res_Res$stableprop_getter(): kotlin/Int // com.compose.sdl.res/com_compose_sdl_res_Res$stableprop_getter|com_compose_sdl_res_Res$stableprop_getter(){}[0]
|
final fun com.compose.sdl.res/com_compose_sdl_res_Res$stableprop_getter(): kotlin/Int // com.compose.sdl.res/com_compose_sdl_res_Res$stableprop_getter|com_compose_sdl_res_Res$stableprop_getter(){}[0]
|
||||||
final fun com.compose.sdl.res/com_compose_sdl_res_Res_drawable$stableprop_getter(): kotlin/Int // com.compose.sdl.res/com_compose_sdl_res_Res_drawable$stableprop_getter|com_compose_sdl_res_Res_drawable$stableprop_getter(){}[0]
|
final fun com.compose.sdl.res/com_compose_sdl_res_Res_drawable$stableprop_getter(): kotlin/Int // com.compose.sdl.res/com_compose_sdl_res_Res_drawable$stableprop_getter|com_compose_sdl_res_Res_drawable$stableprop_getter(){}[0]
|
||||||
final fun com.compose.sdl.res/com_compose_sdl_res_Res_files$stableprop_getter(): kotlin/Int // com.compose.sdl.res/com_compose_sdl_res_Res_files$stableprop_getter|com_compose_sdl_res_Res_files$stableprop_getter(){}[0]
|
final fun com.compose.sdl.res/com_compose_sdl_res_Res_files$stableprop_getter(): kotlin/Int // com.compose.sdl.res/com_compose_sdl_res_Res_files$stableprop_getter|com_compose_sdl_res_Res_files$stableprop_getter(){}[0]
|
||||||
final fun com.compose.sdl.res/com_compose_sdl_res_ResourcePainter$stableprop_getter(): kotlin/Int // com.compose.sdl.res/com_compose_sdl_res_ResourcePainter$stableprop_getter|com_compose_sdl_res_ResourcePainter$stableprop_getter(){}[0]
|
|
||||||
final fun com.compose.sdl.res/painterResource(kotlin/String): androidx.compose.ui.graphics.painter/Painter // com.compose.sdl.res/painterResource|painterResource(kotlin.String){}[0]
|
final fun com.compose.sdl.res/painterResource(kotlin/String): androidx.compose.ui.graphics.painter/Painter // com.compose.sdl.res/painterResource|painterResource(kotlin.String){}[0]
|
||||||
final fun com.compose.sdl.res/painterResource(kotlin/String, com.compose.sdl.res/ResourceKind): androidx.compose.ui.graphics.painter/Painter // com.compose.sdl.res/painterResource|painterResource(kotlin.String;com.compose.sdl.res.ResourceKind){}[0]
|
final fun com.compose.sdl.res/painterResource(kotlin/String, com.compose.sdl.res/ResourceKind): androidx.compose.ui.graphics.painter/Painter // com.compose.sdl.res/painterResource|painterResource(kotlin.String;com.compose.sdl.res.ResourceKind){}[0]
|
||||||
final fun com.compose.sdl.res/resourceKindForPath(kotlin/String): com.compose.sdl.res/ResourceKind // com.compose.sdl.res/resourceKindForPath|resourceKindForPath(kotlin.String){}[0]
|
|
||||||
final inline fun (androidx.compose.ui.graphics.drawscope/DrawScope).androidx.compose.ui.graphics.drawscope/clipPath(androidx.compose.ui.graphics/Path, androidx.compose.ui.graphics/ClipOp = ..., kotlin/Function1<androidx.compose.ui.graphics.drawscope/DrawScope, kotlin/Unit>) // androidx.compose.ui.graphics.drawscope/clipPath|clipPath@androidx.compose.ui.graphics.drawscope.DrawScope(androidx.compose.ui.graphics.Path;androidx.compose.ui.graphics.ClipOp;kotlin.Function1<androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit>){}[0]
|
final inline fun (androidx.compose.ui.graphics.drawscope/DrawScope).androidx.compose.ui.graphics.drawscope/clipPath(androidx.compose.ui.graphics/Path, androidx.compose.ui.graphics/ClipOp = ..., kotlin/Function1<androidx.compose.ui.graphics.drawscope/DrawScope, kotlin/Unit>) // androidx.compose.ui.graphics.drawscope/clipPath|clipPath@androidx.compose.ui.graphics.drawscope.DrawScope(androidx.compose.ui.graphics.Path;androidx.compose.ui.graphics.ClipOp;kotlin.Function1<androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit>){}[0]
|
||||||
final inline fun (androidx.compose.ui.graphics.drawscope/DrawScope).androidx.compose.ui.graphics.drawscope/clipRect(kotlin/Float = ..., kotlin/Float = ..., kotlin/Float = ..., kotlin/Float = ..., androidx.compose.ui.graphics/ClipOp = ..., kotlin/Function1<androidx.compose.ui.graphics.drawscope/DrawScope, kotlin/Unit>) // androidx.compose.ui.graphics.drawscope/clipRect|clipRect@androidx.compose.ui.graphics.drawscope.DrawScope(kotlin.Float;kotlin.Float;kotlin.Float;kotlin.Float;androidx.compose.ui.graphics.ClipOp;kotlin.Function1<androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit>){}[0]
|
final inline fun (androidx.compose.ui.graphics.drawscope/DrawScope).androidx.compose.ui.graphics.drawscope/clipRect(kotlin/Float = ..., kotlin/Float = ..., kotlin/Float = ..., kotlin/Float = ..., androidx.compose.ui.graphics/ClipOp = ..., kotlin/Function1<androidx.compose.ui.graphics.drawscope/DrawScope, kotlin/Unit>) // androidx.compose.ui.graphics.drawscope/clipRect|clipRect@androidx.compose.ui.graphics.drawscope.DrawScope(kotlin.Float;kotlin.Float;kotlin.Float;kotlin.Float;androidx.compose.ui.graphics.ClipOp;kotlin.Function1<androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit>){}[0]
|
||||||
final inline fun (androidx.compose.ui.graphics.drawscope/DrawScope).androidx.compose.ui.graphics.drawscope/draw(androidx.compose.ui.unit/Density, androidx.compose.ui.unit/LayoutDirection, androidx.compose.ui.graphics/Canvas, androidx.compose.ui.geometry/Size, androidx.compose.ui.graphics.layer/GraphicsLayer? = ..., kotlin/Function1<androidx.compose.ui.graphics.drawscope/DrawScope, kotlin/Unit>) // androidx.compose.ui.graphics.drawscope/draw|draw@androidx.compose.ui.graphics.drawscope.DrawScope(androidx.compose.ui.unit.Density;androidx.compose.ui.unit.LayoutDirection;androidx.compose.ui.graphics.Canvas;androidx.compose.ui.geometry.Size;androidx.compose.ui.graphics.layer.GraphicsLayer?;kotlin.Function1<androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit>){}[0]
|
final inline fun (androidx.compose.ui.graphics.drawscope/DrawScope).androidx.compose.ui.graphics.drawscope/draw(androidx.compose.ui.unit/Density, androidx.compose.ui.unit/LayoutDirection, androidx.compose.ui.graphics/Canvas, androidx.compose.ui.geometry/Size, androidx.compose.ui.graphics.layer/GraphicsLayer? = ..., kotlin/Function1<androidx.compose.ui.graphics.drawscope/DrawScope, kotlin/Unit>) // androidx.compose.ui.graphics.drawscope/draw|draw@androidx.compose.ui.graphics.drawscope.DrawScope(androidx.compose.ui.unit.Density;androidx.compose.ui.unit.LayoutDirection;androidx.compose.ui.graphics.Canvas;androidx.compose.ui.geometry.Size;androidx.compose.ui.graphics.layer.GraphicsLayer?;kotlin.Function1<androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit>){}[0]
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@ import com.compose.sdl.res.ResourceKind
|
|||||||
the given rect, and this seam carries no ui-layout (ContentScale) dependency -
|
the given rect, and this seam carries no ui-layout (ContentScale) dependency -
|
||||||
keeping :ui-graphics free of :ui.
|
keeping :ui-graphics free of :ui.
|
||||||
*/
|
*/
|
||||||
interface NativePainterCanvas {
|
internal interface NativePainterCanvas {
|
||||||
fun drawNativePainter(
|
fun drawNativePainter(
|
||||||
inResourcePath: String,
|
inResourcePath: String,
|
||||||
inKind: ResourceKind,
|
inKind: ResourceKind,
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@ package com.compose.sdl.graphics
|
|||||||
carries a `commands: List<PathCommand>` that the SDL3 / Skia renderers walk
|
carries a `commands: List<PathCommand>` that the SDL3 / Skia renderers walk
|
||||||
directly. No official equivalent, so lives in com.compose.sdl
|
directly. No official equivalent, so lives in com.compose.sdl
|
||||||
per FIDELITY.md's relocate rule. */
|
per FIDELITY.md's relocate rule. */
|
||||||
sealed interface PathCommand {
|
internal sealed interface PathCommand {
|
||||||
class MoveTo(val x: Float, val y: Float) : PathCommand
|
class MoveTo(val x: Float, val y: Float) : PathCommand
|
||||||
class LineTo(val x: Float, val y: Float) : PathCommand
|
class LineTo(val x: Float, val y: Float) : PathCommand
|
||||||
class QuadTo(val cx: Float, val cy: Float, val x: Float, val y: Float) : PathCommand
|
class QuadTo(val cx: Float, val cy: Float, val x: Float, val y: Float) : PathCommand
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@ import androidx.compose.ui.geometry.RoundRect
|
|||||||
`Canvas.restore()` - the backend composites and pops it on the matching
|
`Canvas.restore()` - the backend composites and pops it on the matching
|
||||||
`restore()`.
|
`restore()`.
|
||||||
*/
|
*/
|
||||||
interface NativeShapeClipCanvas {
|
internal interface NativeShapeClipCanvas {
|
||||||
// Pushes a rounded-rect clip for subsequent draws until the matching
|
// Pushes a rounded-rect clip for subsequent draws until the matching
|
||||||
// Canvas.restore(). [inRoundRect] is in the CURRENT canvas-local coordinate
|
// Canvas.restore(). [inRoundRect] is in the CURRENT canvas-local coordinate
|
||||||
// space (the same space draw calls use after the enclosing translate).
|
// space (the same space draw calls use after the enclosing translate).
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@ package com.compose.sdl.res
|
|||||||
Not supported: <group> transforms (paths are flattened, transforms ignored),
|
Not supported: <group> transforms (paths are flattened, transforms ignored),
|
||||||
gradients, clip-paths, trim-path. This covers the common single-/multi-path
|
gradients, clip-paths, trim-path. This covers the common single-/multi-path
|
||||||
icon case such as exported Material icons. */
|
icon case such as exported Material icons. */
|
||||||
object AndroidVectorToSvg {
|
internal object AndroidVectorToSvg {
|
||||||
|
|
||||||
// Tag values never contain '>' (pathData is letters/digits/.,- /space) and
|
// Tag values never contain '>' (pathData is letters/digits/.,- /space) and
|
||||||
// quoted attribute values can't either, so [^>] within a tag is safe.
|
// quoted attribute values can't either, so [^>] within a tag is safe.
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ enum class ResourceKind {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Picks a ResourceKind from a file extension. */
|
/** Picks a ResourceKind from a file extension. */
|
||||||
fun resourceKindForPath(path: String): ResourceKind {
|
internal fun resourceKindForPath(path: String): ResourceKind {
|
||||||
val vExt = path.substringAfterLast('.', "").lowercase()
|
val vExt = path.substringAfterLast('.', "").lowercase()
|
||||||
return when (vExt) {
|
return when (vExt) {
|
||||||
"svg" -> ResourceKind.Svg
|
"svg" -> ResourceKind.Svg
|
||||||
|
|||||||
@@ -27,17 +27,17 @@ import com.compose.sdl.graphics.NativePainterCanvas
|
|||||||
If the Canvas doesn't implement NativePainterCanvas (offscreen layer capture
|
If the Canvas doesn't implement NativePainterCanvas (offscreen layer capture
|
||||||
for graphics-layer effects) the paint is skipped.
|
for graphics-layer effects) the paint is skipped.
|
||||||
*/
|
*/
|
||||||
class ResourcePainter internal constructor(
|
internal class ResourcePainter internal constructor(
|
||||||
val resourcePath: String,
|
val resourcePath: String,
|
||||||
val kind: ResourceKind,
|
val kind: ResourceKind,
|
||||||
) : Painter() {
|
) : Painter() {
|
||||||
|
|
||||||
private var fAlpha: Float = 1f
|
private var mAlpha: Float = 1f
|
||||||
|
|
||||||
override val intrinsicSize: Size
|
override val intrinsicSize: Size
|
||||||
get() = currentImageLoader.intrinsicSize(resourcePath, kind)
|
get() = currentImageLoader.intrinsicSize(resourcePath, kind)
|
||||||
|
|
||||||
override fun applyAlpha(alpha: Float): Boolean { fAlpha = alpha; return true }
|
override fun applyAlpha(alpha: Float): Boolean { mAlpha = alpha; return true }
|
||||||
|
|
||||||
override fun DrawScope.onDraw() {
|
override fun DrawScope.onDraw() {
|
||||||
drawIntoCanvas { vCanvas ->
|
drawIntoCanvas { vCanvas ->
|
||||||
@@ -45,7 +45,7 @@ class ResourcePainter internal constructor(
|
|||||||
// translate + scaledSize, so we paint the resource FillBounds into the
|
// translate + scaledSize, so we paint the resource FillBounds into the
|
||||||
// full DrawScope size. Alpha is threaded via applyAlpha.
|
// full DrawScope size. Alpha is threaded via applyAlpha.
|
||||||
(vCanvas as? NativePainterCanvas)?.drawNativePainter(
|
(vCanvas as? NativePainterCanvas)?.drawNativePainter(
|
||||||
resourcePath, kind, 0f, 0f, size.width, size.height, fAlpha,
|
resourcePath, kind, 0f, 0f, size.width, size.height, mAlpha,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-7
@@ -28,12 +28,12 @@ import kotlinx.atomicfu.locks.synchronized
|
|||||||
*/
|
*/
|
||||||
object NativeReleaseQueue {
|
object NativeReleaseQueue {
|
||||||
|
|
||||||
private val fLock = SynchronizedObject()
|
private val mLock = SynchronizedObject()
|
||||||
private var fPending = ArrayList<() -> Unit>()
|
private var mPending = ArrayList<() -> Unit>()
|
||||||
|
|
||||||
/** Enqueue a release action. Safe to call from any thread. */
|
/** Enqueue a release action. Safe to call from any thread. */
|
||||||
fun enqueue(action: () -> Unit) {
|
fun enqueue(action: () -> Unit) {
|
||||||
synchronized(fLock) { fPending.add(action) }
|
synchronized(mLock) { mPending.add(action) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -41,10 +41,10 @@ object NativeReleaseQueue {
|
|||||||
* renderer APIs that aren't thread-safe. Returns the number drained.
|
* renderer APIs that aren't thread-safe. Returns the number drained.
|
||||||
*/
|
*/
|
||||||
fun drain(): Int {
|
fun drain(): Int {
|
||||||
val vBatch = synchronized(fLock) {
|
val vBatch = synchronized(mLock) {
|
||||||
if (fPending.isEmpty()) return 0
|
if (mPending.isEmpty()) return 0
|
||||||
val vTaken = fPending
|
val vTaken = mPending
|
||||||
fPending = ArrayList()
|
mPending = ArrayList()
|
||||||
vTaken
|
vTaken
|
||||||
}
|
}
|
||||||
for (vAction in vBatch) {
|
for (vAction in vBatch) {
|
||||||
|
|||||||
+23
-23
@@ -38,7 +38,7 @@ class SkiaImageCache {
|
|||||||
// (registerMemoryResource - e.g. downloaded PNGs) from growing image memory
|
// (registerMemoryResource - e.g. downloaded PNGs) from growing image memory
|
||||||
// without limit (issue #2). On-screen images stay hot; an evicted one just
|
// without limit (issue #2). On-screen images stay hot; an evicted one just
|
||||||
// re-decodes on next use.
|
// re-decodes on next use.
|
||||||
private val fCache = LinkedHashMap<String, Image?>()
|
private val mCache = LinkedHashMap<String, Image?>()
|
||||||
|
|
||||||
// SVG / Android-vector kinds are RESOLUTION-INDEPENDENT: instead of caching one
|
// SVG / Android-vector kinds are RESOLUTION-INDEPENDENT: instead of caching one
|
||||||
// intrinsic-size raster and letting drawImageRect upscale it (blurry when an icon
|
// intrinsic-size raster and letting drawImageRect upscale it (blurry when an icon
|
||||||
@@ -46,8 +46,8 @@ class SkiaImageCache {
|
|||||||
// destination pixel size and cache that raster keyed by "path@WxH". Mirrors
|
// destination pixel size and cache that raster keyed by "path@WxH". Mirrors
|
||||||
// upstream's size-driven DrawCache for SVGPainter. Intrinsic dims are cached
|
// upstream's size-driven DrawCache for SVGPainter. Intrinsic dims are cached
|
||||||
// separately so the layout pass (intrinsicSize) doesn't force a raster.
|
// separately so the layout pass (intrinsicSize) doesn't force a raster.
|
||||||
private val fSvgRasterCache = LinkedHashMap<String, Image?>()
|
private val mSvgRasterCache = LinkedHashMap<String, Image?>()
|
||||||
private val fSvgIntrinsic = HashMap<String, androidx.compose.ui.geometry.Size>()
|
private val mSvgIntrinsic = HashMap<String, androidx.compose.ui.geometry.Size>()
|
||||||
|
|
||||||
fun intrinsicSize(inPath: String, inKind: ResourceKind): androidx.compose.ui.geometry.Size {
|
fun intrinsicSize(inPath: String, inKind: ResourceKind): androidx.compose.ui.geometry.Size {
|
||||||
if (inKind == ResourceKind.Svg || inKind == ResourceKind.AndroidVector) {
|
if (inKind == ResourceKind.Svg || inKind == ResourceKind.AndroidVector) {
|
||||||
@@ -58,18 +58,18 @@ class SkiaImageCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun get(inPath: String, inKind: ResourceKind): Image? {
|
private fun get(inPath: String, inKind: ResourceKind): Image? {
|
||||||
if (fCache.containsKey(inPath)) {
|
if (mCache.containsKey(inPath)) {
|
||||||
// Re-insert to mark most-recently-used (LinkedHashMap keeps first =
|
// Re-insert to mark most-recently-used (LinkedHashMap keeps first =
|
||||||
// least-recently-used for eviction below).
|
// least-recently-used for eviction below).
|
||||||
val vExisting = fCache.remove(inPath)
|
val vExisting = mCache.remove(inPath)
|
||||||
fCache[inPath] = vExisting
|
mCache[inPath] = vExisting
|
||||||
return vExisting
|
return vExisting
|
||||||
}
|
}
|
||||||
val vImage = decode(inPath, inKind)
|
val vImage = decode(inPath, inKind)
|
||||||
fCache[inPath] = vImage
|
mCache[inPath] = vImage
|
||||||
if (fCache.size > MAX_CACHED_IMAGES) {
|
if (mCache.size > MAX_CACHED_IMAGES) {
|
||||||
val vEldest = fCache.keys.firstOrNull()
|
val vEldest = mCache.keys.firstOrNull()
|
||||||
if (vEldest != null) fCache.remove(vEldest)?.close()
|
if (vEldest != null) mCache.remove(vEldest)?.close()
|
||||||
}
|
}
|
||||||
return vImage
|
return vImage
|
||||||
}
|
}
|
||||||
@@ -136,7 +136,7 @@ class SkiaImageCache {
|
|||||||
|
|
||||||
/** Intrinsic (viewport) size of the vector, parsed once and cached. */
|
/** Intrinsic (viewport) size of the vector, parsed once and cached. */
|
||||||
private fun svgIntrinsicSize(inPath: String, inKind: ResourceKind): androidx.compose.ui.geometry.Size {
|
private fun svgIntrinsicSize(inPath: String, inKind: ResourceKind): androidx.compose.ui.geometry.Size {
|
||||||
fSvgIntrinsic[inPath]?.let { return it }
|
mSvgIntrinsic[inPath]?.let { return it }
|
||||||
val vBytes = svgBytes(inPath, inKind) ?: return androidx.compose.ui.geometry.Size.Unspecified
|
val vBytes = svgBytes(inPath, inKind) ?: return androidx.compose.ui.geometry.Size.Unspecified
|
||||||
val vSize = runCatching {
|
val vSize = runCatching {
|
||||||
val vDom = SVGDOM(Data.makeFromBytes(vBytes))
|
val vDom = SVGDOM(Data.makeFromBytes(vBytes))
|
||||||
@@ -146,7 +146,7 @@ class SkiaImageCache {
|
|||||||
vDom.close()
|
vDom.close()
|
||||||
androidx.compose.ui.geometry.Size(vW, vH)
|
androidx.compose.ui.geometry.Size(vW, vH)
|
||||||
}.getOrDefault(androidx.compose.ui.geometry.Size.Unspecified)
|
}.getOrDefault(androidx.compose.ui.geometry.Size.Unspecified)
|
||||||
fSvgIntrinsic[inPath] = vSize
|
mSvgIntrinsic[inPath] = vSize
|
||||||
return vSize
|
return vSize
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,9 +157,9 @@ class SkiaImageCache {
|
|||||||
val vWpx = inW.roundToInt().coerceAtLeast(1)
|
val vWpx = inW.roundToInt().coerceAtLeast(1)
|
||||||
val vHpx = inH.roundToInt().coerceAtLeast(1)
|
val vHpx = inH.roundToInt().coerceAtLeast(1)
|
||||||
val vKey = "$inPath@${vWpx}x$vHpx"
|
val vKey = "$inPath@${vWpx}x$vHpx"
|
||||||
val vImg = if (fSvgRasterCache.containsKey(vKey)) {
|
val vImg = if (mSvgRasterCache.containsKey(vKey)) {
|
||||||
val vExisting = fSvgRasterCache.remove(vKey)
|
val vExisting = mSvgRasterCache.remove(vKey)
|
||||||
fSvgRasterCache[vKey] = vExisting
|
mSvgRasterCache[vKey] = vExisting
|
||||||
vExisting
|
vExisting
|
||||||
} else {
|
} else {
|
||||||
val vIntrinsic = svgIntrinsicSize(inPath, inKind)
|
val vIntrinsic = svgIntrinsicSize(inPath, inKind)
|
||||||
@@ -167,9 +167,9 @@ class SkiaImageCache {
|
|||||||
val vRaster = if (vBytes != null && vIntrinsic.isSpecified) {
|
val vRaster = if (vBytes != null && vIntrinsic.isSpecified) {
|
||||||
rasterizeSvgAt(vBytes, vIntrinsic.width, vIntrinsic.height, vWpx, vHpx)
|
rasterizeSvgAt(vBytes, vIntrinsic.width, vIntrinsic.height, vWpx, vHpx)
|
||||||
} else null
|
} else null
|
||||||
fSvgRasterCache[vKey] = vRaster
|
mSvgRasterCache[vKey] = vRaster
|
||||||
if (fSvgRasterCache.size > MAX_CACHED_IMAGES) {
|
if (mSvgRasterCache.size > MAX_CACHED_IMAGES) {
|
||||||
fSvgRasterCache.keys.firstOrNull()?.let { fSvgRasterCache.remove(it)?.close() }
|
mSvgRasterCache.keys.firstOrNull()?.let { mSvgRasterCache.remove(it)?.close() }
|
||||||
}
|
}
|
||||||
vRaster
|
vRaster
|
||||||
} ?: return
|
} ?: return
|
||||||
@@ -183,11 +183,11 @@ class SkiaImageCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun destroy() {
|
fun destroy() {
|
||||||
for (vImg in fCache.values) vImg?.close()
|
for (vImg in mCache.values) vImg?.close()
|
||||||
fCache.clear()
|
mCache.clear()
|
||||||
for (vImg in fSvgRasterCache.values) vImg?.close()
|
for (vImg in mSvgRasterCache.values) vImg?.close()
|
||||||
fSvgRasterCache.clear()
|
mSvgRasterCache.clear()
|
||||||
fSvgIntrinsic.clear()
|
mSvgIntrinsic.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,35 +18,35 @@ package com.compose.sdl.icons
|
|||||||
the renderer where to find the bytes. */
|
the renderer where to find the bytes. */
|
||||||
object IconFont {
|
object IconFont {
|
||||||
|
|
||||||
private val fFonts = mutableMapOf<String, ByteArray>()
|
private val mFonts = mutableMapOf<String, ByteArray>()
|
||||||
// Families that render as single variable-axis glyphs (Material Symbols),
|
// Families that render as single variable-axis glyphs (Material Symbols),
|
||||||
// vs ordinary text fonts - icon families are drawn one codepoint at a time,
|
// vs ordinary text fonts - icon families are drawn one codepoint at a time,
|
||||||
// while text families (e.g. a bundled monospace) go through normal
|
// while text families (e.g. a bundled monospace) go through normal
|
||||||
// full-string rendering.
|
// full-string rendering.
|
||||||
private val fIconFamilies = mutableSetOf<String>()
|
private val mIconFamilies = mutableSetOf<String>()
|
||||||
|
|
||||||
/** Register a TEXT font family's bytes (rendered as full strings; e.g. a
|
/** Register a TEXT font family's bytes (rendered as full strings; e.g. a
|
||||||
bundled monospace). Idempotent - a second call with the same family
|
bundled monospace). Idempotent - a second call with the same family
|
||||||
replaces the previous bytes. */
|
replaces the previous bytes. */
|
||||||
fun register(inFamily: String, inBytes: ByteArray) {
|
fun register(inFamily: String, inBytes: ByteArray) {
|
||||||
fFonts[inFamily] = inBytes
|
mFonts[inFamily] = inBytes
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Register an ICON font family (Material Symbols et al.) - variable-axis.
|
/** Register an ICON font family (Material Symbols et al.) - variable-axis.
|
||||||
Same byte store as register(), but also flags the family as an icon
|
Same byte store as register(), but also flags the family as an icon
|
||||||
font. */
|
font. */
|
||||||
fun registerIcon(inFamily: String, inBytes: ByteArray) {
|
fun registerIcon(inFamily: String, inBytes: ByteArray) {
|
||||||
fFonts[inFamily] = inBytes
|
mFonts[inFamily] = inBytes
|
||||||
fIconFamilies += inFamily
|
mIconFamilies += inFamily
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Bytes for a registered family, or null if not registered. Renderers
|
/** Bytes for a registered family, or null if not registered. Renderers
|
||||||
fall back to the default font when this returns null. */
|
fall back to the default font when this returns null. */
|
||||||
fun bytesFor(inFamily: String): ByteArray? = fFonts[inFamily]
|
fun bytesFor(inFamily: String): ByteArray? = mFonts[inFamily]
|
||||||
|
|
||||||
/** True only for families registered via registerIcon() - icon fonts that
|
/** True only for families registered via registerIcon() - icon fonts that
|
||||||
need single-glyph (variable-axis) rendering, not text fonts. */
|
need single-glyph (variable-axis) rendering, not text fonts. */
|
||||||
fun isIconFamily(inFamily: String): Boolean = inFamily in fIconFamilies
|
fun isIconFamily(inFamily: String): Boolean = inFamily in mIconFamilies
|
||||||
|
|
||||||
val families: Set<String> get() = fFonts.keys
|
val families: Set<String> get() = mFonts.keys
|
||||||
}
|
}
|
||||||
|
|||||||
+109
-146
@@ -1370,17 +1370,6 @@ abstract interface androidx.compose.ui/UiMediaScope { // androidx.compose.ui/UiM
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract interface com.compose.sdl.renderer.skia/SkiaBridge { // com.compose.sdl.renderer.skia/SkiaBridge|null[0]
|
|
||||||
abstract val canvas // com.compose.sdl.renderer.skia/SkiaBridge.canvas|{}canvas[0]
|
|
||||||
abstract fun <get-canvas>(): org.jetbrains.skia/Canvas // com.compose.sdl.renderer.skia/SkiaBridge.canvas.<get-canvas>|<get-canvas>(){}[0]
|
|
||||||
|
|
||||||
abstract fun destroy() // com.compose.sdl.renderer.skia/SkiaBridge.destroy|destroy(){}[0]
|
|
||||||
abstract fun ensureSize(kotlin/Int, kotlin/Int): kotlin/Boolean // com.compose.sdl.renderer.skia/SkiaBridge.ensureSize|ensureSize(kotlin.Int;kotlin.Int){}[0]
|
|
||||||
abstract fun present() // com.compose.sdl.renderer.skia/SkiaBridge.present|present(){}[0]
|
|
||||||
abstract fun snapshot(): org.jetbrains.skia/Image? // com.compose.sdl.renderer.skia/SkiaBridge.snapshot|snapshot(){}[0]
|
|
||||||
abstract fun snapshotBgra(): kotlin/Triple<kotlin/Int, kotlin/Int, kotlin/ByteArray>? // com.compose.sdl.renderer.skia/SkiaBridge.snapshotBgra|snapshotBgra(){}[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract interface com.compose.sdl/ComposeWindowScope { // com.compose.sdl/ComposeWindowScope|null[0]
|
abstract interface com.compose.sdl/ComposeWindowScope { // com.compose.sdl/ComposeWindowScope|null[0]
|
||||||
abstract val window // com.compose.sdl/ComposeWindowScope.window|{}window[0]
|
abstract val window // com.compose.sdl/ComposeWindowScope.window|{}window[0]
|
||||||
abstract fun <get-window>(): com.compose.sdl/ComposeNativeWindow // com.compose.sdl/ComposeWindowScope.window.<get-window>|<get-window>(){}[0]
|
abstract fun <get-window>(): com.compose.sdl/ComposeNativeWindow // com.compose.sdl/ComposeWindowScope.window.<get-window>|<get-window>(){}[0]
|
||||||
@@ -1392,10 +1381,10 @@ abstract interface com.compose.sdl/RenderBackend { // com.compose.sdl/RenderBack
|
|||||||
|
|
||||||
abstract fun beginFrame(kotlin/Float) // com.compose.sdl/RenderBackend.beginFrame|beginFrame(kotlin.Float){}[0]
|
abstract fun beginFrame(kotlin/Float) // com.compose.sdl/RenderBackend.beginFrame|beginFrame(kotlin.Float){}[0]
|
||||||
abstract fun destroy() // com.compose.sdl/RenderBackend.destroy|destroy(){}[0]
|
abstract fun destroy() // com.compose.sdl/RenderBackend.destroy|destroy(){}[0]
|
||||||
|
abstract fun drawRoot(kotlin/Function1<androidx.compose.ui.graphics/Canvas, kotlin/Unit>) // com.compose.sdl/RenderBackend.drawRoot|drawRoot(kotlin.Function1<androidx.compose.ui.graphics.Canvas,kotlin.Unit>){}[0]
|
||||||
abstract fun endFrame() // com.compose.sdl/RenderBackend.endFrame|endFrame(){}[0]
|
abstract fun endFrame() // com.compose.sdl/RenderBackend.endFrame|endFrame(){}[0]
|
||||||
abstract fun ensureSize(kotlin/Int, kotlin/Int): kotlin/Boolean // com.compose.sdl/RenderBackend.ensureSize|ensureSize(kotlin.Int;kotlin.Int){}[0]
|
abstract fun ensureSize(kotlin/Int, kotlin/Int): kotlin/Boolean // com.compose.sdl/RenderBackend.ensureSize|ensureSize(kotlin.Int;kotlin.Int){}[0]
|
||||||
abstract fun snapshotBgra(): kotlin/Triple<kotlin/Int, kotlin/Int, kotlin/ByteArray>? // com.compose.sdl/RenderBackend.snapshotBgra|snapshotBgra(){}[0]
|
abstract fun snapshotBgra(): kotlin/Triple<kotlin/Int, kotlin/Int, kotlin/ByteArray>? // com.compose.sdl/RenderBackend.snapshotBgra|snapshotBgra(){}[0]
|
||||||
open fun drawRoot(kotlin/Function1<androidx.compose.ui.graphics/Canvas, kotlin/Unit>) // com.compose.sdl/RenderBackend.drawRoot|drawRoot(kotlin.Function1<androidx.compose.ui.graphics.Canvas,kotlin.Unit>){}[0]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface androidx.compose.ui.autofill/ContentDataType { // androidx.compose.ui.autofill/ContentDataType|null[0]
|
sealed interface androidx.compose.ui.autofill/ContentDataType { // androidx.compose.ui.autofill/ContentDataType|null[0]
|
||||||
@@ -2788,23 +2777,6 @@ final class androidx.compose.ui/CombinedModifier : androidx.compose.ui/Modifier
|
|||||||
final fun toString(): kotlin/String // androidx.compose.ui/CombinedModifier.toString|toString(){}[0]
|
final fun toString(): kotlin/String // androidx.compose.ui/CombinedModifier.toString|toString(){}[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
final class com.compose.sdl.input/LegacyPointerEvent { // com.compose.sdl.input/LegacyPointerEvent|null[0]
|
|
||||||
constructor <init>(kotlin/Float, kotlin/Float, androidx.compose.ui.input.pointer/PointerEventType, androidx.compose.ui.input.pointer/PointerButton = ...) // com.compose.sdl.input/LegacyPointerEvent.<init>|<init>(kotlin.Float;kotlin.Float;androidx.compose.ui.input.pointer.PointerEventType;androidx.compose.ui.input.pointer.PointerButton){}[0]
|
|
||||||
|
|
||||||
final val button // com.compose.sdl.input/LegacyPointerEvent.button|{}button[0]
|
|
||||||
final fun <get-button>(): androidx.compose.ui.input.pointer/PointerButton // com.compose.sdl.input/LegacyPointerEvent.button.<get-button>|<get-button>(){}[0]
|
|
||||||
final val type // com.compose.sdl.input/LegacyPointerEvent.type|{}type[0]
|
|
||||||
final fun <get-type>(): androidx.compose.ui.input.pointer/PointerEventType // com.compose.sdl.input/LegacyPointerEvent.type.<get-type>|<get-type>(){}[0]
|
|
||||||
final val x // com.compose.sdl.input/LegacyPointerEvent.x|{}x[0]
|
|
||||||
final fun <get-x>(): kotlin/Float // com.compose.sdl.input/LegacyPointerEvent.x.<get-x>|<get-x>(){}[0]
|
|
||||||
final val y // com.compose.sdl.input/LegacyPointerEvent.y|{}y[0]
|
|
||||||
final fun <get-y>(): kotlin/Float // com.compose.sdl.input/LegacyPointerEvent.y.<get-y>|<get-y>(){}[0]
|
|
||||||
|
|
||||||
final fun equals(kotlin/Any?): kotlin/Boolean // com.compose.sdl.input/LegacyPointerEvent.equals|equals(kotlin.Any?){}[0]
|
|
||||||
final fun hashCode(): kotlin/Int // com.compose.sdl.input/LegacyPointerEvent.hashCode|hashCode(){}[0]
|
|
||||||
final fun toString(): kotlin/String // com.compose.sdl.input/LegacyPointerEvent.toString|toString(){}[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
final class com.compose.sdl.node/ComposeRootHost { // com.compose.sdl.node/ComposeRootHost|null[0]
|
final class com.compose.sdl.node/ComposeRootHost { // com.compose.sdl.node/ComposeRootHost|null[0]
|
||||||
constructor <init>(kotlin/Float = ...) // com.compose.sdl.node/ComposeRootHost.<init>|<init>(kotlin.Float){}[0]
|
constructor <init>(kotlin/Float = ...) // com.compose.sdl.node/ComposeRootHost.<init>|<init>(kotlin.Float){}[0]
|
||||||
|
|
||||||
@@ -2850,37 +2822,12 @@ final class com.compose.sdl.node/ComposeRootHost { // com.compose.sdl.node/Compo
|
|||||||
final fun setWindowFocused(kotlin/Boolean) // com.compose.sdl.node/ComposeRootHost.setWindowFocused|setWindowFocused(kotlin.Boolean){}[0]
|
final fun setWindowFocused(kotlin/Boolean) // com.compose.sdl.node/ComposeRootHost.setWindowFocused|setWindowFocused(kotlin.Boolean){}[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
final class com.compose.sdl.renderer.skia/SkiaSurfaceBridge : com.compose.sdl.renderer.skia/SkiaBridge { // com.compose.sdl.renderer.skia/SkiaSurfaceBridge|null[0]
|
|
||||||
constructor <init>(com.compose.sdl/SDL3Backend) // com.compose.sdl.renderer.skia/SkiaSurfaceBridge.<init>|<init>(com.compose.sdl.SDL3Backend){}[0]
|
|
||||||
|
|
||||||
final val canvas // com.compose.sdl.renderer.skia/SkiaSurfaceBridge.canvas|{}canvas[0]
|
|
||||||
final fun <get-canvas>(): org.jetbrains.skia/Canvas // com.compose.sdl.renderer.skia/SkiaSurfaceBridge.canvas.<get-canvas>|<get-canvas>(){}[0]
|
|
||||||
final val height // com.compose.sdl.renderer.skia/SkiaSurfaceBridge.height|{}height[0]
|
|
||||||
final fun <get-height>(): kotlin/Int // com.compose.sdl.renderer.skia/SkiaSurfaceBridge.height.<get-height>|<get-height>(){}[0]
|
|
||||||
final val width // com.compose.sdl.renderer.skia/SkiaSurfaceBridge.width|{}width[0]
|
|
||||||
final fun <get-width>(): kotlin/Int // com.compose.sdl.renderer.skia/SkiaSurfaceBridge.width.<get-width>|<get-width>(){}[0]
|
|
||||||
|
|
||||||
final fun destroy() // com.compose.sdl.renderer.skia/SkiaSurfaceBridge.destroy|destroy(){}[0]
|
|
||||||
final fun ensureSize(kotlin/Int, kotlin/Int): kotlin/Boolean // com.compose.sdl.renderer.skia/SkiaSurfaceBridge.ensureSize|ensureSize(kotlin.Int;kotlin.Int){}[0]
|
|
||||||
final fun present() // com.compose.sdl.renderer.skia/SkiaSurfaceBridge.present|present(){}[0]
|
|
||||||
final fun snapshot(): org.jetbrains.skia/Image? // com.compose.sdl.renderer.skia/SkiaSurfaceBridge.snapshot|snapshot(){}[0]
|
|
||||||
final fun snapshotBgra(): kotlin/Triple<kotlin/Int, kotlin/Int, kotlin/ByteArray>? // com.compose.sdl.renderer.skia/SkiaSurfaceBridge.snapshotBgra|snapshotBgra(){}[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
final class com.compose.sdl.window/PopupExitHandle { // com.compose.sdl.window/PopupExitHandle|null[0]
|
|
||||||
final val isExiting // com.compose.sdl.window/PopupExitHandle.isExiting|{}isExiting[0]
|
|
||||||
final fun <get-isExiting>(): androidx.compose.runtime/State<kotlin/Boolean> // com.compose.sdl.window/PopupExitHandle.isExiting.<get-isExiting>|<get-isExiting>(){}[0]
|
|
||||||
|
|
||||||
final fun enableExitTransition() // com.compose.sdl.window/PopupExitHandle.enableExitTransition|enableExitTransition(){}[0]
|
|
||||||
final fun finish() // com.compose.sdl.window/PopupExitHandle.finish|finish(){}[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
final class com.compose.sdl.window/PopupHostState { // com.compose.sdl.window/PopupHostState|null[0]
|
final class com.compose.sdl.window/PopupHostState { // com.compose.sdl.window/PopupHostState|null[0]
|
||||||
final fun notifyOutsidePress(kotlin/Int, kotlin/Int) // com.compose.sdl.window/PopupHostState.notifyOutsidePress|notifyOutsidePress(kotlin.Int;kotlin.Int){}[0]
|
final fun notifyOutsidePress(kotlin/Int, kotlin/Int) // com.compose.sdl.window/PopupHostState.notifyOutsidePress|notifyOutsidePress(kotlin.Int;kotlin.Int){}[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
final class com.compose.sdl/ComposeNativeWindow { // com.compose.sdl/ComposeNativeWindow|null[0]
|
final class com.compose.sdl/ComposeNativeWindow { // com.compose.sdl/ComposeNativeWindow|null[0]
|
||||||
constructor <init>(com.compose.sdl/SDL3Backend, com.compose.sdl/GpuMode, kotlin/String) // com.compose.sdl/ComposeNativeWindow.<init>|<init>(com.compose.sdl.SDL3Backend;com.compose.sdl.GpuMode;kotlin.String){}[0]
|
constructor <init>(com.compose.sdl/Sdl3Backend, com.compose.sdl/GpuMode, kotlin/String) // com.compose.sdl/ComposeNativeWindow.<init>|<init>(com.compose.sdl.Sdl3Backend;com.compose.sdl.GpuMode;kotlin.String){}[0]
|
||||||
|
|
||||||
final val fps // com.compose.sdl/ComposeNativeWindow.fps|{}fps[0]
|
final val fps // com.compose.sdl/ComposeNativeWindow.fps|{}fps[0]
|
||||||
final fun <get-fps>(): kotlin/Int // com.compose.sdl/ComposeNativeWindow.fps.<get-fps>|<get-fps>(){}[0]
|
final fun <get-fps>(): kotlin/Int // com.compose.sdl/ComposeNativeWindow.fps.<get-fps>|<get-fps>(){}[0]
|
||||||
@@ -2888,14 +2835,24 @@ final class com.compose.sdl/ComposeNativeWindow { // com.compose.sdl/ComposeNati
|
|||||||
final fun <get-gpuMode>(): com.compose.sdl/GpuMode // com.compose.sdl/ComposeNativeWindow.gpuMode.<get-gpuMode>|<get-gpuMode>(){}[0]
|
final fun <get-gpuMode>(): com.compose.sdl/GpuMode // com.compose.sdl/ComposeNativeWindow.gpuMode.<get-gpuMode>|<get-gpuMode>(){}[0]
|
||||||
final val height // com.compose.sdl/ComposeNativeWindow.height|{}height[0]
|
final val height // com.compose.sdl/ComposeNativeWindow.height|{}height[0]
|
||||||
final fun <get-height>(): kotlin/Int // com.compose.sdl/ComposeNativeWindow.height.<get-height>|<get-height>(){}[0]
|
final fun <get-height>(): kotlin/Int // com.compose.sdl/ComposeNativeWindow.height.<get-height>|<get-height>(){}[0]
|
||||||
|
final val isAlwaysOnTop // com.compose.sdl/ComposeNativeWindow.isAlwaysOnTop|{}isAlwaysOnTop[0]
|
||||||
|
final fun <get-isAlwaysOnTop>(): kotlin/Boolean // com.compose.sdl/ComposeNativeWindow.isAlwaysOnTop.<get-isAlwaysOnTop>|<get-isAlwaysOnTop>(){}[0]
|
||||||
final val isCloseRequested // com.compose.sdl/ComposeNativeWindow.isCloseRequested|{}isCloseRequested[0]
|
final val isCloseRequested // com.compose.sdl/ComposeNativeWindow.isCloseRequested|{}isCloseRequested[0]
|
||||||
final fun <get-isCloseRequested>(): kotlin/Boolean // com.compose.sdl/ComposeNativeWindow.isCloseRequested.<get-isCloseRequested>|<get-isCloseRequested>(){}[0]
|
final fun <get-isCloseRequested>(): kotlin/Boolean // com.compose.sdl/ComposeNativeWindow.isCloseRequested.<get-isCloseRequested>|<get-isCloseRequested>(){}[0]
|
||||||
|
final val isEnabled // com.compose.sdl/ComposeNativeWindow.isEnabled|{}isEnabled[0]
|
||||||
|
final fun <get-isEnabled>(): kotlin/Boolean // com.compose.sdl/ComposeNativeWindow.isEnabled.<get-isEnabled>|<get-isEnabled>(){}[0]
|
||||||
|
final val isFocusable // com.compose.sdl/ComposeNativeWindow.isFocusable|{}isFocusable[0]
|
||||||
|
final fun <get-isFocusable>(): kotlin/Boolean // com.compose.sdl/ComposeNativeWindow.isFocusable.<get-isFocusable>|<get-isFocusable>(){}[0]
|
||||||
final val isFullscreen // com.compose.sdl/ComposeNativeWindow.isFullscreen|{}isFullscreen[0]
|
final val isFullscreen // com.compose.sdl/ComposeNativeWindow.isFullscreen|{}isFullscreen[0]
|
||||||
final fun <get-isFullscreen>(): kotlin/Boolean // com.compose.sdl/ComposeNativeWindow.isFullscreen.<get-isFullscreen>|<get-isFullscreen>(){}[0]
|
final fun <get-isFullscreen>(): kotlin/Boolean // com.compose.sdl/ComposeNativeWindow.isFullscreen.<get-isFullscreen>|<get-isFullscreen>(){}[0]
|
||||||
final val isMaximized // com.compose.sdl/ComposeNativeWindow.isMaximized|{}isMaximized[0]
|
final val isMaximized // com.compose.sdl/ComposeNativeWindow.isMaximized|{}isMaximized[0]
|
||||||
final fun <get-isMaximized>(): kotlin/Boolean // com.compose.sdl/ComposeNativeWindow.isMaximized.<get-isMaximized>|<get-isMaximized>(){}[0]
|
final fun <get-isMaximized>(): kotlin/Boolean // com.compose.sdl/ComposeNativeWindow.isMaximized.<get-isMaximized>|<get-isMaximized>(){}[0]
|
||||||
final val isMinimized // com.compose.sdl/ComposeNativeWindow.isMinimized|{}isMinimized[0]
|
final val isMinimized // com.compose.sdl/ComposeNativeWindow.isMinimized|{}isMinimized[0]
|
||||||
final fun <get-isMinimized>(): kotlin/Boolean // com.compose.sdl/ComposeNativeWindow.isMinimized.<get-isMinimized>|<get-isMinimized>(){}[0]
|
final fun <get-isMinimized>(): kotlin/Boolean // com.compose.sdl/ComposeNativeWindow.isMinimized.<get-isMinimized>|<get-isMinimized>(){}[0]
|
||||||
|
final val isResizable // com.compose.sdl/ComposeNativeWindow.isResizable|{}isResizable[0]
|
||||||
|
final fun <get-isResizable>(): kotlin/Boolean // com.compose.sdl/ComposeNativeWindow.isResizable.<get-isResizable>|<get-isResizable>(){}[0]
|
||||||
|
final val isUndecorated // com.compose.sdl/ComposeNativeWindow.isUndecorated|{}isUndecorated[0]
|
||||||
|
final fun <get-isUndecorated>(): kotlin/Boolean // com.compose.sdl/ComposeNativeWindow.isUndecorated.<get-isUndecorated>|<get-isUndecorated>(){}[0]
|
||||||
final val pixelDensity // com.compose.sdl/ComposeNativeWindow.pixelDensity|{}pixelDensity[0]
|
final val pixelDensity // com.compose.sdl/ComposeNativeWindow.pixelDensity|{}pixelDensity[0]
|
||||||
final fun <get-pixelDensity>(): kotlin/Float // com.compose.sdl/ComposeNativeWindow.pixelDensity.<get-pixelDensity>|<get-pixelDensity>(){}[0]
|
final fun <get-pixelDensity>(): kotlin/Float // com.compose.sdl/ComposeNativeWindow.pixelDensity.<get-pixelDensity>|<get-pixelDensity>(){}[0]
|
||||||
final val pixelHeight // com.compose.sdl/ComposeNativeWindow.pixelHeight|{}pixelHeight[0]
|
final val pixelHeight // com.compose.sdl/ComposeNativeWindow.pixelHeight|{}pixelHeight[0]
|
||||||
@@ -2915,50 +2872,79 @@ final class com.compose.sdl/ComposeNativeWindow { // com.compose.sdl/ComposeNati
|
|||||||
final fun minimize() // com.compose.sdl/ComposeNativeWindow.minimize|minimize(){}[0]
|
final fun minimize() // com.compose.sdl/ComposeNativeWindow.minimize|minimize(){}[0]
|
||||||
final fun onResized() // com.compose.sdl/ComposeNativeWindow.onResized|onResized(){}[0]
|
final fun onResized() // com.compose.sdl/ComposeNativeWindow.onResized|onResized(){}[0]
|
||||||
final fun raise() // com.compose.sdl/ComposeNativeWindow.raise|raise(){}[0]
|
final fun raise() // com.compose.sdl/ComposeNativeWindow.raise|raise(){}[0]
|
||||||
|
final fun rawSdlHandles(): com.compose.sdl/RawSdlHandles // com.compose.sdl/ComposeNativeWindow.rawSdlHandles|rawSdlHandles(){}[0]
|
||||||
final fun requestCloseFromUser(): kotlin/Boolean // com.compose.sdl/ComposeNativeWindow.requestCloseFromUser|requestCloseFromUser(){}[0]
|
final fun requestCloseFromUser(): kotlin/Boolean // com.compose.sdl/ComposeNativeWindow.requestCloseFromUser|requestCloseFromUser(){}[0]
|
||||||
final fun restore() // com.compose.sdl/ComposeNativeWindow.restore|restore(){}[0]
|
final fun restore() // com.compose.sdl/ComposeNativeWindow.restore|restore(){}[0]
|
||||||
|
final fun setAlwaysOnTop(kotlin/Boolean) // com.compose.sdl/ComposeNativeWindow.setAlwaysOnTop|setAlwaysOnTop(kotlin.Boolean){}[0]
|
||||||
|
final fun setEnabled(kotlin/Boolean) // com.compose.sdl/ComposeNativeWindow.setEnabled|setEnabled(kotlin.Boolean){}[0]
|
||||||
|
final fun setFocusable(kotlin/Boolean) // com.compose.sdl/ComposeNativeWindow.setFocusable|setFocusable(kotlin.Boolean){}[0]
|
||||||
final fun setFullscreen(kotlin/Boolean) // com.compose.sdl/ComposeNativeWindow.setFullscreen|setFullscreen(kotlin.Boolean){}[0]
|
final fun setFullscreen(kotlin/Boolean) // com.compose.sdl/ComposeNativeWindow.setFullscreen|setFullscreen(kotlin.Boolean){}[0]
|
||||||
final fun setOnCloseRequest(kotlin/Function0<kotlin/Boolean>?) // com.compose.sdl/ComposeNativeWindow.setOnCloseRequest|setOnCloseRequest(kotlin.Function0<kotlin.Boolean>?){}[0]
|
final fun setOnCloseRequest(kotlin/Function0<kotlin/Boolean>?) // com.compose.sdl/ComposeNativeWindow.setOnCloseRequest|setOnCloseRequest(kotlin.Function0<kotlin.Boolean>?){}[0]
|
||||||
final fun setOnKeyShortcut(kotlin/Function1<androidx.compose.ui.input.key/KeyEvent, kotlin/Boolean>?) // com.compose.sdl/ComposeNativeWindow.setOnKeyShortcut|setOnKeyShortcut(kotlin.Function1<androidx.compose.ui.input.key.KeyEvent,kotlin.Boolean>?){}[0]
|
final fun setOnKeyShortcut(kotlin/Function1<androidx.compose.ui.input.key/KeyEvent, kotlin/Boolean>?) // com.compose.sdl/ComposeNativeWindow.setOnKeyShortcut|setOnKeyShortcut(kotlin.Function1<androidx.compose.ui.input.key.KeyEvent,kotlin.Boolean>?){}[0]
|
||||||
|
final fun setOpacity(kotlin/Float) // com.compose.sdl/ComposeNativeWindow.setOpacity|setOpacity(kotlin.Float){}[0]
|
||||||
|
final fun setResizable(kotlin/Boolean) // com.compose.sdl/ComposeNativeWindow.setResizable|setResizable(kotlin.Boolean){}[0]
|
||||||
final fun setSize(kotlin/Int, kotlin/Int) // com.compose.sdl/ComposeNativeWindow.setSize|setSize(kotlin.Int;kotlin.Int){}[0]
|
final fun setSize(kotlin/Int, kotlin/Int) // com.compose.sdl/ComposeNativeWindow.setSize|setSize(kotlin.Int;kotlin.Int){}[0]
|
||||||
final fun setTitle(kotlin/String) // com.compose.sdl/ComposeNativeWindow.setTitle|setTitle(kotlin.String){}[0]
|
final fun setTitle(kotlin/String) // com.compose.sdl/ComposeNativeWindow.setTitle|setTitle(kotlin.String){}[0]
|
||||||
|
final fun setUndecorated(kotlin/Boolean) // com.compose.sdl/ComposeNativeWindow.setUndecorated|setUndecorated(kotlin.Boolean){}[0]
|
||||||
final fun toggleFullscreen() // com.compose.sdl/ComposeNativeWindow.toggleFullscreen|toggleFullscreen(){}[0]
|
final fun toggleFullscreen() // com.compose.sdl/ComposeNativeWindow.toggleFullscreen|toggleFullscreen(){}[0]
|
||||||
final fun updateFps(kotlin/Int) // com.compose.sdl/ComposeNativeWindow.updateFps|updateFps(kotlin.Int){}[0]
|
final fun updateFps(kotlin/Int) // com.compose.sdl/ComposeNativeWindow.updateFps|updateFps(kotlin.Int){}[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
final class com.compose.sdl/SDL3Backend { // com.compose.sdl/SDL3Backend|null[0]
|
final class com.compose.sdl/RawSdlHandles { // com.compose.sdl/RawSdlHandles|null[0]
|
||||||
constructor <init>(kotlin/String = ..., kotlin/Int = ..., kotlin/Int = ..., com.compose.sdl/GpuMode = ..., kotlin.collections/List<kotlin/String> = ..., kotlin.collections/List<kotlin/String> = ...) // com.compose.sdl/SDL3Backend.<init>|<init>(kotlin.String;kotlin.Int;kotlin.Int;com.compose.sdl.GpuMode;kotlin.collections.List<kotlin.String>;kotlin.collections.List<kotlin.String>){}[0]
|
constructor <init>(kotlinx.cinterop/CPointer<out kotlinx.cinterop/CPointed>?, kotlinx.cinterop/CPointer<out kotlinx.cinterop/CPointed>?, kotlinx.cinterop/CPointer<out kotlinx.cinterop/CPointed>?, kotlinx.cinterop/CPointer<out kotlinx.cinterop/CPointed>?) // com.compose.sdl/RawSdlHandles.<init>|<init>(kotlinx.cinterop.CPointer<out|kotlinx.cinterop.CPointed>?;kotlinx.cinterop.CPointer<out|kotlinx.cinterop.CPointed>?;kotlinx.cinterop.CPointer<out|kotlinx.cinterop.CPointed>?;kotlinx.cinterop.CPointer<out|kotlinx.cinterop.CPointed>?){}[0]
|
||||||
|
|
||||||
final val gpuMode // com.compose.sdl/SDL3Backend.gpuMode|{}gpuMode[0]
|
final val glContext // com.compose.sdl/RawSdlHandles.glContext|{}glContext[0]
|
||||||
final fun <get-gpuMode>(): com.compose.sdl/GpuMode // com.compose.sdl/SDL3Backend.gpuMode.<get-gpuMode>|<get-gpuMode>(){}[0]
|
final fun <get-glContext>(): kotlinx.cinterop/CPointer<out kotlinx.cinterop/CPointed>? // com.compose.sdl/RawSdlHandles.glContext.<get-glContext>|<get-glContext>(){}[0]
|
||||||
final val pixelDensity // com.compose.sdl/SDL3Backend.pixelDensity|{}pixelDensity[0]
|
final val metalView // com.compose.sdl/RawSdlHandles.metalView|{}metalView[0]
|
||||||
final fun <get-pixelDensity>(): kotlin/Float // com.compose.sdl/SDL3Backend.pixelDensity.<get-pixelDensity>|<get-pixelDensity>(){}[0]
|
final fun <get-metalView>(): kotlinx.cinterop/CPointer<out kotlinx.cinterop/CPointed>? // com.compose.sdl/RawSdlHandles.metalView.<get-metalView>|<get-metalView>(){}[0]
|
||||||
final val windowId // com.compose.sdl/SDL3Backend.windowId|{}windowId[0]
|
final val renderer // com.compose.sdl/RawSdlHandles.renderer|{}renderer[0]
|
||||||
final fun <get-windowId>(): kotlin/UInt // com.compose.sdl/SDL3Backend.windowId.<get-windowId>|<get-windowId>(){}[0]
|
final fun <get-renderer>(): kotlinx.cinterop/CPointer<out kotlinx.cinterop/CPointed>? // com.compose.sdl/RawSdlHandles.renderer.<get-renderer>|<get-renderer>(){}[0]
|
||||||
|
final val window // com.compose.sdl/RawSdlHandles.window|{}window[0]
|
||||||
|
final fun <get-window>(): kotlinx.cinterop/CPointer<out kotlinx.cinterop/CPointed>? // com.compose.sdl/RawSdlHandles.window.<get-window>|<get-window>(){}[0]
|
||||||
|
|
||||||
final var glContext // com.compose.sdl/SDL3Backend.glContext|{}glContext[0]
|
final fun component1(): kotlinx.cinterop/CPointer<out kotlinx.cinterop/CPointed>? // com.compose.sdl/RawSdlHandles.component1|component1(){}[0]
|
||||||
final fun <get-glContext>(): kotlinx.cinterop/CPointer<out kotlinx.cinterop/CPointed>? // com.compose.sdl/SDL3Backend.glContext.<get-glContext>|<get-glContext>(){}[0]
|
final fun component2(): kotlinx.cinterop/CPointer<out kotlinx.cinterop/CPointed>? // com.compose.sdl/RawSdlHandles.component2|component2(){}[0]
|
||||||
final var metalView // com.compose.sdl/SDL3Backend.metalView|{}metalView[0]
|
final fun component3(): kotlinx.cinterop/CPointer<out kotlinx.cinterop/CPointed>? // com.compose.sdl/RawSdlHandles.component3|component3(){}[0]
|
||||||
final fun <get-metalView>(): kotlinx.cinterop/CPointer<out kotlinx.cinterop/CPointed>? // com.compose.sdl/SDL3Backend.metalView.<get-metalView>|<get-metalView>(){}[0]
|
final fun component4(): kotlinx.cinterop/CPointer<out kotlinx.cinterop/CPointed>? // com.compose.sdl/RawSdlHandles.component4|component4(){}[0]
|
||||||
final var pixelHeight // com.compose.sdl/SDL3Backend.pixelHeight|{}pixelHeight[0]
|
final fun copy(kotlinx.cinterop/CPointer<out kotlinx.cinterop/CPointed>? = ..., kotlinx.cinterop/CPointer<out kotlinx.cinterop/CPointed>? = ..., kotlinx.cinterop/CPointer<out kotlinx.cinterop/CPointed>? = ..., kotlinx.cinterop/CPointer<out kotlinx.cinterop/CPointed>? = ...): com.compose.sdl/RawSdlHandles // com.compose.sdl/RawSdlHandles.copy|copy(kotlinx.cinterop.CPointer<out|kotlinx.cinterop.CPointed>?;kotlinx.cinterop.CPointer<out|kotlinx.cinterop.CPointed>?;kotlinx.cinterop.CPointer<out|kotlinx.cinterop.CPointed>?;kotlinx.cinterop.CPointer<out|kotlinx.cinterop.CPointed>?){}[0]
|
||||||
final fun <get-pixelHeight>(): kotlin/Int // com.compose.sdl/SDL3Backend.pixelHeight.<get-pixelHeight>|<get-pixelHeight>(){}[0]
|
final fun equals(kotlin/Any?): kotlin/Boolean // com.compose.sdl/RawSdlHandles.equals|equals(kotlin.Any?){}[0]
|
||||||
final var pixelWidth // com.compose.sdl/SDL3Backend.pixelWidth|{}pixelWidth[0]
|
final fun hashCode(): kotlin/Int // com.compose.sdl/RawSdlHandles.hashCode|hashCode(){}[0]
|
||||||
final fun <get-pixelWidth>(): kotlin/Int // com.compose.sdl/SDL3Backend.pixelWidth.<get-pixelWidth>|<get-pixelWidth>(){}[0]
|
final fun toString(): kotlin/String // com.compose.sdl/RawSdlHandles.toString|toString(){}[0]
|
||||||
final var renderer // com.compose.sdl/SDL3Backend.renderer|{}renderer[0]
|
}
|
||||||
final fun <get-renderer>(): kotlinx.cinterop/CPointer<out kotlinx.cinterop/CPointed>? // com.compose.sdl/SDL3Backend.renderer.<get-renderer>|<get-renderer>(){}[0]
|
|
||||||
final var vsyncEnabled // com.compose.sdl/SDL3Backend.vsyncEnabled|{}vsyncEnabled[0]
|
|
||||||
final fun <get-vsyncEnabled>(): kotlin/Boolean // com.compose.sdl/SDL3Backend.vsyncEnabled.<get-vsyncEnabled>|<get-vsyncEnabled>(){}[0]
|
|
||||||
final var window // com.compose.sdl/SDL3Backend.window|{}window[0]
|
|
||||||
final fun <get-window>(): kotlinx.cinterop/CPointer<out kotlinx.cinterop/CPointed>? // com.compose.sdl/SDL3Backend.window.<get-window>|<get-window>(){}[0]
|
|
||||||
final var windowHeight // com.compose.sdl/SDL3Backend.windowHeight|{}windowHeight[0]
|
|
||||||
final fun <get-windowHeight>(): kotlin/Int // com.compose.sdl/SDL3Backend.windowHeight.<get-windowHeight>|<get-windowHeight>(){}[0]
|
|
||||||
final var windowWidth // com.compose.sdl/SDL3Backend.windowWidth|{}windowWidth[0]
|
|
||||||
final fun <get-windowWidth>(): kotlin/Int // com.compose.sdl/SDL3Backend.windowWidth.<get-windowWidth>|<get-windowWidth>(){}[0]
|
|
||||||
|
|
||||||
final fun applyThemeIcon() // com.compose.sdl/SDL3Backend.applyThemeIcon|applyThemeIcon(){}[0]
|
final class com.compose.sdl/Sdl3Backend { // com.compose.sdl/Sdl3Backend|null[0]
|
||||||
final fun destroy(kotlin/Boolean = ...) // com.compose.sdl/SDL3Backend.destroy|destroy(kotlin.Boolean){}[0]
|
constructor <init>(kotlin/String = ..., kotlin/Int = ..., kotlin/Int = ..., com.compose.sdl/GpuMode = ..., kotlin.collections/List<kotlin/String> = ..., kotlin.collections/List<kotlin/String> = ..., kotlin/Boolean = ..., kotlin/Boolean = ..., kotlin/Boolean = ..., kotlin/Boolean = ..., kotlin/Boolean = ...) // com.compose.sdl/Sdl3Backend.<init>|<init>(kotlin.String;kotlin.Int;kotlin.Int;com.compose.sdl.GpuMode;kotlin.collections.List<kotlin.String>;kotlin.collections.List<kotlin.String>;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;kotlin.Boolean){}[0]
|
||||||
final fun init(): kotlin/Boolean // com.compose.sdl/SDL3Backend.init|init(){}[0]
|
|
||||||
final fun updateWindowSize() // com.compose.sdl/SDL3Backend.updateWindowSize|updateWindowSize(){}[0]
|
final val gpuMode // com.compose.sdl/Sdl3Backend.gpuMode|{}gpuMode[0]
|
||||||
|
final fun <get-gpuMode>(): com.compose.sdl/GpuMode // com.compose.sdl/Sdl3Backend.gpuMode.<get-gpuMode>|<get-gpuMode>(){}[0]
|
||||||
|
final val pixelDensity // com.compose.sdl/Sdl3Backend.pixelDensity|{}pixelDensity[0]
|
||||||
|
final fun <get-pixelDensity>(): kotlin/Float // com.compose.sdl/Sdl3Backend.pixelDensity.<get-pixelDensity>|<get-pixelDensity>(){}[0]
|
||||||
|
final val windowId // com.compose.sdl/Sdl3Backend.windowId|{}windowId[0]
|
||||||
|
final fun <get-windowId>(): kotlin/UInt // com.compose.sdl/Sdl3Backend.windowId.<get-windowId>|<get-windowId>(){}[0]
|
||||||
|
|
||||||
|
final var glContext // com.compose.sdl/Sdl3Backend.glContext|{}glContext[0]
|
||||||
|
final fun <get-glContext>(): kotlinx.cinterop/CPointer<out kotlinx.cinterop/CPointed>? // com.compose.sdl/Sdl3Backend.glContext.<get-glContext>|<get-glContext>(){}[0]
|
||||||
|
final var metalView // com.compose.sdl/Sdl3Backend.metalView|{}metalView[0]
|
||||||
|
final fun <get-metalView>(): kotlinx.cinterop/CPointer<out kotlinx.cinterop/CPointed>? // com.compose.sdl/Sdl3Backend.metalView.<get-metalView>|<get-metalView>(){}[0]
|
||||||
|
final var pixelHeight // com.compose.sdl/Sdl3Backend.pixelHeight|{}pixelHeight[0]
|
||||||
|
final fun <get-pixelHeight>(): kotlin/Int // com.compose.sdl/Sdl3Backend.pixelHeight.<get-pixelHeight>|<get-pixelHeight>(){}[0]
|
||||||
|
final var pixelWidth // com.compose.sdl/Sdl3Backend.pixelWidth|{}pixelWidth[0]
|
||||||
|
final fun <get-pixelWidth>(): kotlin/Int // com.compose.sdl/Sdl3Backend.pixelWidth.<get-pixelWidth>|<get-pixelWidth>(){}[0]
|
||||||
|
final var renderer // com.compose.sdl/Sdl3Backend.renderer|{}renderer[0]
|
||||||
|
final fun <get-renderer>(): kotlinx.cinterop/CPointer<out kotlinx.cinterop/CPointed>? // com.compose.sdl/Sdl3Backend.renderer.<get-renderer>|<get-renderer>(){}[0]
|
||||||
|
final var vsyncEnabled // com.compose.sdl/Sdl3Backend.vsyncEnabled|{}vsyncEnabled[0]
|
||||||
|
final fun <get-vsyncEnabled>(): kotlin/Boolean // com.compose.sdl/Sdl3Backend.vsyncEnabled.<get-vsyncEnabled>|<get-vsyncEnabled>(){}[0]
|
||||||
|
final var window // com.compose.sdl/Sdl3Backend.window|{}window[0]
|
||||||
|
final fun <get-window>(): kotlinx.cinterop/CPointer<out kotlinx.cinterop/CPointed>? // com.compose.sdl/Sdl3Backend.window.<get-window>|<get-window>(){}[0]
|
||||||
|
final var windowHeight // com.compose.sdl/Sdl3Backend.windowHeight|{}windowHeight[0]
|
||||||
|
final fun <get-windowHeight>(): kotlin/Int // com.compose.sdl/Sdl3Backend.windowHeight.<get-windowHeight>|<get-windowHeight>(){}[0]
|
||||||
|
final var windowWidth // com.compose.sdl/Sdl3Backend.windowWidth|{}windowWidth[0]
|
||||||
|
final fun <get-windowWidth>(): kotlin/Int // com.compose.sdl/Sdl3Backend.windowWidth.<get-windowWidth>|<get-windowWidth>(){}[0]
|
||||||
|
|
||||||
|
final fun applyThemeIcon() // com.compose.sdl/Sdl3Backend.applyThemeIcon|applyThemeIcon(){}[0]
|
||||||
|
final fun destroy(kotlin/Boolean = ...) // com.compose.sdl/Sdl3Backend.destroy|destroy(kotlin.Boolean){}[0]
|
||||||
|
final fun init(): kotlin/Boolean // com.compose.sdl/Sdl3Backend.init|init(){}[0]
|
||||||
|
final fun updateWindowSize() // com.compose.sdl/Sdl3Backend.updateWindowSize|updateWindowSize(){}[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
final value class <#A: kotlin/Any?> androidx.compose.ui/SessionMutex { // androidx.compose.ui/SessionMutex|null[0]
|
final value class <#A: kotlin/Any?> androidx.compose.ui/SessionMutex { // androidx.compose.ui/SessionMutex|null[0]
|
||||||
@@ -4161,16 +4147,25 @@ sealed class com.compose.sdl/AppEvent { // com.compose.sdl/AppEvent|null[0]
|
|||||||
}
|
}
|
||||||
|
|
||||||
final class Pointer : com.compose.sdl/AppEvent { // com.compose.sdl/AppEvent.Pointer|null[0]
|
final class Pointer : com.compose.sdl/AppEvent { // com.compose.sdl/AppEvent.Pointer|null[0]
|
||||||
constructor <init>(com.compose.sdl.input/LegacyPointerEvent, kotlin/UInt = ...) // com.compose.sdl/AppEvent.Pointer.<init>|<init>(com.compose.sdl.input.LegacyPointerEvent;kotlin.UInt){}[0]
|
constructor <init>(kotlin/Float, kotlin/Float, androidx.compose.ui.input.pointer/PointerEventType, androidx.compose.ui.input.pointer/PointerButton = ..., kotlin/UInt = ...) // com.compose.sdl/AppEvent.Pointer.<init>|<init>(kotlin.Float;kotlin.Float;androidx.compose.ui.input.pointer.PointerEventType;androidx.compose.ui.input.pointer.PointerButton;kotlin.UInt){}[0]
|
||||||
|
|
||||||
final val event // com.compose.sdl/AppEvent.Pointer.event|{}event[0]
|
final val button // com.compose.sdl/AppEvent.Pointer.button|{}button[0]
|
||||||
final fun <get-event>(): com.compose.sdl.input/LegacyPointerEvent // com.compose.sdl/AppEvent.Pointer.event.<get-event>|<get-event>(){}[0]
|
final fun <get-button>(): androidx.compose.ui.input.pointer/PointerButton // com.compose.sdl/AppEvent.Pointer.button.<get-button>|<get-button>(){}[0]
|
||||||
|
final val type // com.compose.sdl/AppEvent.Pointer.type|{}type[0]
|
||||||
|
final fun <get-type>(): androidx.compose.ui.input.pointer/PointerEventType // com.compose.sdl/AppEvent.Pointer.type.<get-type>|<get-type>(){}[0]
|
||||||
final val windowId // com.compose.sdl/AppEvent.Pointer.windowId|{}windowId[0]
|
final val windowId // com.compose.sdl/AppEvent.Pointer.windowId|{}windowId[0]
|
||||||
final fun <get-windowId>(): kotlin/UInt // com.compose.sdl/AppEvent.Pointer.windowId.<get-windowId>|<get-windowId>(){}[0]
|
final fun <get-windowId>(): kotlin/UInt // com.compose.sdl/AppEvent.Pointer.windowId.<get-windowId>|<get-windowId>(){}[0]
|
||||||
|
final val x // com.compose.sdl/AppEvent.Pointer.x|{}x[0]
|
||||||
|
final fun <get-x>(): kotlin/Float // com.compose.sdl/AppEvent.Pointer.x.<get-x>|<get-x>(){}[0]
|
||||||
|
final val y // com.compose.sdl/AppEvent.Pointer.y|{}y[0]
|
||||||
|
final fun <get-y>(): kotlin/Float // com.compose.sdl/AppEvent.Pointer.y.<get-y>|<get-y>(){}[0]
|
||||||
|
|
||||||
final fun component1(): com.compose.sdl.input/LegacyPointerEvent // com.compose.sdl/AppEvent.Pointer.component1|component1(){}[0]
|
final fun component1(): kotlin/Float // com.compose.sdl/AppEvent.Pointer.component1|component1(){}[0]
|
||||||
final fun component2(): kotlin/UInt // com.compose.sdl/AppEvent.Pointer.component2|component2(){}[0]
|
final fun component2(): kotlin/Float // com.compose.sdl/AppEvent.Pointer.component2|component2(){}[0]
|
||||||
final fun copy(com.compose.sdl.input/LegacyPointerEvent = ..., kotlin/UInt = ...): com.compose.sdl/AppEvent.Pointer // com.compose.sdl/AppEvent.Pointer.copy|copy(com.compose.sdl.input.LegacyPointerEvent;kotlin.UInt){}[0]
|
final fun component3(): androidx.compose.ui.input.pointer/PointerEventType // com.compose.sdl/AppEvent.Pointer.component3|component3(){}[0]
|
||||||
|
final fun component4(): androidx.compose.ui.input.pointer/PointerButton // com.compose.sdl/AppEvent.Pointer.component4|component4(){}[0]
|
||||||
|
final fun component5(): kotlin/UInt // com.compose.sdl/AppEvent.Pointer.component5|component5(){}[0]
|
||||||
|
final fun copy(kotlin/Float = ..., kotlin/Float = ..., androidx.compose.ui.input.pointer/PointerEventType = ..., androidx.compose.ui.input.pointer/PointerButton = ..., kotlin/UInt = ...): com.compose.sdl/AppEvent.Pointer // com.compose.sdl/AppEvent.Pointer.copy|copy(kotlin.Float;kotlin.Float;androidx.compose.ui.input.pointer.PointerEventType;androidx.compose.ui.input.pointer.PointerButton;kotlin.UInt){}[0]
|
||||||
final fun equals(kotlin/Any?): kotlin/Boolean // com.compose.sdl/AppEvent.Pointer.equals|equals(kotlin.Any?){}[0]
|
final fun equals(kotlin/Any?): kotlin/Boolean // com.compose.sdl/AppEvent.Pointer.equals|equals(kotlin.Any?){}[0]
|
||||||
final fun hashCode(): kotlin/Int // com.compose.sdl/AppEvent.Pointer.hashCode|hashCode(){}[0]
|
final fun hashCode(): kotlin/Int // com.compose.sdl/AppEvent.Pointer.hashCode|hashCode(){}[0]
|
||||||
final fun toString(): kotlin/String // com.compose.sdl/AppEvent.Pointer.toString|toString(){}[0]
|
final fun toString(): kotlin/String // com.compose.sdl/AppEvent.Pointer.toString|toString(){}[0]
|
||||||
@@ -4293,22 +4288,23 @@ sealed class com.compose.sdl/AppEvent { // com.compose.sdl/AppEvent|null[0]
|
|||||||
}
|
}
|
||||||
|
|
||||||
sealed class com.compose.sdl/GpuMode { // com.compose.sdl/GpuMode|null[0]
|
sealed class com.compose.sdl/GpuMode { // com.compose.sdl/GpuMode|null[0]
|
||||||
sealed class Skia : com.compose.sdl/GpuMode { // com.compose.sdl/GpuMode.Skia|null[0]
|
final val isGpuAccelerated // com.compose.sdl/GpuMode.isGpuAccelerated|{}isGpuAccelerated[0]
|
||||||
final object Metal : com.compose.sdl/GpuMode.Skia { // com.compose.sdl/GpuMode.Skia.Metal|null[0]
|
final fun <get-isGpuAccelerated>(): kotlin/Boolean // com.compose.sdl/GpuMode.isGpuAccelerated.<get-isGpuAccelerated>|<get-isGpuAccelerated>(){}[0]
|
||||||
final fun toString(): kotlin/String // com.compose.sdl/GpuMode.Skia.Metal.toString|toString(){}[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
final object OpenGL : com.compose.sdl/GpuMode.Skia { // com.compose.sdl/GpuMode.Skia.OpenGL|null[0]
|
|
||||||
final fun toString(): kotlin/String // com.compose.sdl/GpuMode.Skia.OpenGL.toString|toString(){}[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final object Auto : com.compose.sdl/GpuMode { // com.compose.sdl/GpuMode.Auto|null[0]
|
final object Auto : com.compose.sdl/GpuMode { // com.compose.sdl/GpuMode.Auto|null[0]
|
||||||
final fun toString(): kotlin/String // com.compose.sdl/GpuMode.Auto.toString|toString(){}[0]
|
final fun toString(): kotlin/String // com.compose.sdl/GpuMode.Auto.toString|toString(){}[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
final object Software : com.compose.sdl/GpuMode { // com.compose.sdl/GpuMode.Software|null[0]
|
final object CpuRaster : com.compose.sdl/GpuMode { // com.compose.sdl/GpuMode.CpuRaster|null[0]
|
||||||
final fun toString(): kotlin/String // com.compose.sdl/GpuMode.Software.toString|toString(){}[0]
|
final fun toString(): kotlin/String // com.compose.sdl/GpuMode.CpuRaster.toString|toString(){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
final object Metal : com.compose.sdl/GpuMode { // com.compose.sdl/GpuMode.Metal|null[0]
|
||||||
|
final fun toString(): kotlin/String // com.compose.sdl/GpuMode.Metal.toString|toString(){}[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
final object OpenGL : com.compose.sdl/GpuMode { // com.compose.sdl/GpuMode.OpenGL|null[0]
|
||||||
|
final fun toString(): kotlin/String // com.compose.sdl/GpuMode.OpenGL.toString|toString(){}[0]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4520,27 +4516,6 @@ final object androidx.compose.ui/ComposeUiFlags { // androidx.compose.ui/Compose
|
|||||||
final fun <set-isViewFocusFixEnabled>(kotlin/Boolean) // androidx.compose.ui/ComposeUiFlags.isViewFocusFixEnabled.<set-isViewFocusFixEnabled>|<set-isViewFocusFixEnabled>(kotlin.Boolean){}[0]
|
final fun <set-isViewFocusFixEnabled>(kotlin/Boolean) // androidx.compose.ui/ComposeUiFlags.isViewFocusFixEnabled.<set-isViewFocusFixEnabled>|<set-isViewFocusFixEnabled>(kotlin.Boolean){}[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
final object com.compose.sdl.platform/DefaultViewConfiguration : androidx.compose.ui.platform/ViewConfiguration { // com.compose.sdl.platform/DefaultViewConfiguration|null[0]
|
|
||||||
final val doubleTapMinTimeMillis // com.compose.sdl.platform/DefaultViewConfiguration.doubleTapMinTimeMillis|{}doubleTapMinTimeMillis[0]
|
|
||||||
final fun <get-doubleTapMinTimeMillis>(): kotlin/Long // com.compose.sdl.platform/DefaultViewConfiguration.doubleTapMinTimeMillis.<get-doubleTapMinTimeMillis>|<get-doubleTapMinTimeMillis>(){}[0]
|
|
||||||
final val doubleTapTimeoutMillis // com.compose.sdl.platform/DefaultViewConfiguration.doubleTapTimeoutMillis|{}doubleTapTimeoutMillis[0]
|
|
||||||
final fun <get-doubleTapTimeoutMillis>(): kotlin/Long // com.compose.sdl.platform/DefaultViewConfiguration.doubleTapTimeoutMillis.<get-doubleTapTimeoutMillis>|<get-doubleTapTimeoutMillis>(){}[0]
|
|
||||||
final val handwritingGestureLineMargin // com.compose.sdl.platform/DefaultViewConfiguration.handwritingGestureLineMargin|{}handwritingGestureLineMargin[0]
|
|
||||||
final fun <get-handwritingGestureLineMargin>(): kotlin/Float // com.compose.sdl.platform/DefaultViewConfiguration.handwritingGestureLineMargin.<get-handwritingGestureLineMargin>|<get-handwritingGestureLineMargin>(){}[0]
|
|
||||||
final val handwritingSlop // com.compose.sdl.platform/DefaultViewConfiguration.handwritingSlop|{}handwritingSlop[0]
|
|
||||||
final fun <get-handwritingSlop>(): kotlin/Float // com.compose.sdl.platform/DefaultViewConfiguration.handwritingSlop.<get-handwritingSlop>|<get-handwritingSlop>(){}[0]
|
|
||||||
final val longPressTimeoutMillis // com.compose.sdl.platform/DefaultViewConfiguration.longPressTimeoutMillis|{}longPressTimeoutMillis[0]
|
|
||||||
final fun <get-longPressTimeoutMillis>(): kotlin/Long // com.compose.sdl.platform/DefaultViewConfiguration.longPressTimeoutMillis.<get-longPressTimeoutMillis>|<get-longPressTimeoutMillis>(){}[0]
|
|
||||||
final val maximumFlingVelocity // com.compose.sdl.platform/DefaultViewConfiguration.maximumFlingVelocity|{}maximumFlingVelocity[0]
|
|
||||||
final fun <get-maximumFlingVelocity>(): kotlin/Float // com.compose.sdl.platform/DefaultViewConfiguration.maximumFlingVelocity.<get-maximumFlingVelocity>|<get-maximumFlingVelocity>(){}[0]
|
|
||||||
final val minimumFlingVelocity // com.compose.sdl.platform/DefaultViewConfiguration.minimumFlingVelocity|{}minimumFlingVelocity[0]
|
|
||||||
final fun <get-minimumFlingVelocity>(): kotlin/Float // com.compose.sdl.platform/DefaultViewConfiguration.minimumFlingVelocity.<get-minimumFlingVelocity>|<get-minimumFlingVelocity>(){}[0]
|
|
||||||
final val minimumTouchTargetSize // com.compose.sdl.platform/DefaultViewConfiguration.minimumTouchTargetSize|{}minimumTouchTargetSize[0]
|
|
||||||
final fun <get-minimumTouchTargetSize>(): androidx.compose.ui.unit/DpSize // com.compose.sdl.platform/DefaultViewConfiguration.minimumTouchTargetSize.<get-minimumTouchTargetSize>|<get-minimumTouchTargetSize>(){}[0]
|
|
||||||
final val touchSlop // com.compose.sdl.platform/DefaultViewConfiguration.touchSlop|{}touchSlop[0]
|
|
||||||
final fun <get-touchSlop>(): kotlin/Float // com.compose.sdl.platform/DefaultViewConfiguration.touchSlop.<get-touchSlop>|<get-touchSlop>(){}[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
final object com.compose.sdl.text.input/ImeBridge { // com.compose.sdl.text.input/ImeBridge|null[0]
|
final object com.compose.sdl.text.input/ImeBridge { // com.compose.sdl.text.input/ImeBridge|null[0]
|
||||||
final var onSessionActiveChange // com.compose.sdl.text.input/ImeBridge.onSessionActiveChange|{}onSessionActiveChange[0]
|
final var onSessionActiveChange // com.compose.sdl.text.input/ImeBridge.onSessionActiveChange|{}onSessionActiveChange[0]
|
||||||
final fun <get-onSessionActiveChange>(): kotlin/Function1<kotlin/Boolean, kotlin/Unit>? // com.compose.sdl.text.input/ImeBridge.onSessionActiveChange.<get-onSessionActiveChange>|<get-onSessionActiveChange>(){}[0]
|
final fun <get-onSessionActiveChange>(): kotlin/Function1<kotlin/Boolean, kotlin/Unit>? // com.compose.sdl.text.input/ImeBridge.onSessionActiveChange.<get-onSessionActiveChange>|<get-onSessionActiveChange>(){}[0]
|
||||||
@@ -4854,7 +4829,6 @@ final val androidx.compose.ui/androidx_compose_ui_BiasAlignment_Vertical$stablep
|
|||||||
final val androidx.compose.ui/androidx_compose_ui_CombinedModifier$stableprop // androidx.compose.ui/androidx_compose_ui_CombinedModifier$stableprop|#static{}androidx_compose_ui_CombinedModifier$stableprop[0]
|
final val androidx.compose.ui/androidx_compose_ui_CombinedModifier$stableprop // androidx.compose.ui/androidx_compose_ui_CombinedModifier$stableprop|#static{}androidx_compose_ui_CombinedModifier$stableprop[0]
|
||||||
final val androidx.compose.ui/androidx_compose_ui_ComposeUiFlags$stableprop // androidx.compose.ui/androidx_compose_ui_ComposeUiFlags$stableprop|#static{}androidx_compose_ui_ComposeUiFlags$stableprop[0]
|
final val androidx.compose.ui/androidx_compose_ui_ComposeUiFlags$stableprop // androidx.compose.ui/androidx_compose_ui_ComposeUiFlags$stableprop|#static{}androidx_compose_ui_ComposeUiFlags$stableprop[0]
|
||||||
final val androidx.compose.ui/androidx_compose_ui_Modifier_Node$stableprop // androidx.compose.ui/androidx_compose_ui_Modifier_Node$stableprop|#static{}androidx_compose_ui_Modifier_Node$stableprop[0]
|
final val androidx.compose.ui/androidx_compose_ui_Modifier_Node$stableprop // androidx.compose.ui/androidx_compose_ui_Modifier_Node$stableprop|#static{}androidx_compose_ui_Modifier_Node$stableprop[0]
|
||||||
final val com.compose.sdl.input/com_compose_sdl_input_LegacyPointerEvent$stableprop // com.compose.sdl.input/com_compose_sdl_input_LegacyPointerEvent$stableprop|#static{}com_compose_sdl_input_LegacyPointerEvent$stableprop[0]
|
|
||||||
final val com.compose.sdl.layout/intOffset // com.compose.sdl.layout/intOffset|@androidx.compose.ui.layout.LayoutCoordinates{}intOffset[0]
|
final val com.compose.sdl.layout/intOffset // com.compose.sdl.layout/intOffset|@androidx.compose.ui.layout.LayoutCoordinates{}intOffset[0]
|
||||||
final fun (androidx.compose.ui.layout/LayoutCoordinates).<get-intOffset>(): androidx.compose.ui.unit/IntOffset // com.compose.sdl.layout/intOffset.<get-intOffset>|<get-intOffset>@androidx.compose.ui.layout.LayoutCoordinates(){}[0]
|
final fun (androidx.compose.ui.layout/LayoutCoordinates).<get-intOffset>(): androidx.compose.ui.unit/IntOffset // com.compose.sdl.layout/intOffset.<get-intOffset>|<get-intOffset>@androidx.compose.ui.layout.LayoutCoordinates(){}[0]
|
||||||
final val com.compose.sdl.layout/x // com.compose.sdl.layout/x|@androidx.compose.ui.layout.LayoutCoordinates{}x[0]
|
final val com.compose.sdl.layout/x // com.compose.sdl.layout/x|@androidx.compose.ui.layout.LayoutCoordinates{}x[0]
|
||||||
@@ -4862,14 +4836,9 @@ final val com.compose.sdl.layout/x // com.compose.sdl.layout/x|@androidx.compose
|
|||||||
final val com.compose.sdl.layout/y // com.compose.sdl.layout/y|@androidx.compose.ui.layout.LayoutCoordinates{}y[0]
|
final val com.compose.sdl.layout/y // com.compose.sdl.layout/y|@androidx.compose.ui.layout.LayoutCoordinates{}y[0]
|
||||||
final fun (androidx.compose.ui.layout/LayoutCoordinates).<get-y>(): kotlin/Int // com.compose.sdl.layout/y.<get-y>|<get-y>@androidx.compose.ui.layout.LayoutCoordinates(){}[0]
|
final fun (androidx.compose.ui.layout/LayoutCoordinates).<get-y>(): kotlin/Int // com.compose.sdl.layout/y.<get-y>|<get-y>@androidx.compose.ui.layout.LayoutCoordinates(){}[0]
|
||||||
final val com.compose.sdl.node/com_compose_sdl_node_ComposeRootHost$stableprop // com.compose.sdl.node/com_compose_sdl_node_ComposeRootHost$stableprop|#static{}com_compose_sdl_node_ComposeRootHost$stableprop[0]
|
final val com.compose.sdl.node/com_compose_sdl_node_ComposeRootHost$stableprop // com.compose.sdl.node/com_compose_sdl_node_ComposeRootHost$stableprop|#static{}com_compose_sdl_node_ComposeRootHost$stableprop[0]
|
||||||
final val com.compose.sdl.platform/com_compose_sdl_platform_DefaultViewConfiguration$stableprop // com.compose.sdl.platform/com_compose_sdl_platform_DefaultViewConfiguration$stableprop|#static{}com_compose_sdl_platform_DefaultViewConfiguration$stableprop[0]
|
|
||||||
final val com.compose.sdl.renderer.skia/com_compose_sdl_renderer_skia_SkiaSurfaceBridge$stableprop // com.compose.sdl.renderer.skia/com_compose_sdl_renderer_skia_SkiaSurfaceBridge$stableprop|#static{}com_compose_sdl_renderer_skia_SkiaSurfaceBridge$stableprop[0]
|
|
||||||
final val com.compose.sdl.text.input/com_compose_sdl_text_input_ImeBridge$stableprop // com.compose.sdl.text.input/com_compose_sdl_text_input_ImeBridge$stableprop|#static{}com_compose_sdl_text_input_ImeBridge$stableprop[0]
|
final val com.compose.sdl.text.input/com_compose_sdl_text_input_ImeBridge$stableprop // com.compose.sdl.text.input/com_compose_sdl_text_input_ImeBridge$stableprop|#static{}com_compose_sdl_text_input_ImeBridge$stableprop[0]
|
||||||
final val com.compose.sdl.window/LocalPopupExitHandle // com.compose.sdl.window/LocalPopupExitHandle|{}LocalPopupExitHandle[0]
|
|
||||||
final fun <get-LocalPopupExitHandle>(): androidx.compose.runtime/ProvidableCompositionLocal<com.compose.sdl.window/PopupExitHandle?> // com.compose.sdl.window/LocalPopupExitHandle.<get-LocalPopupExitHandle>|<get-LocalPopupExitHandle>(){}[0]
|
|
||||||
final val com.compose.sdl.window/LocalPopupHost // com.compose.sdl.window/LocalPopupHost|{}LocalPopupHost[0]
|
final val com.compose.sdl.window/LocalPopupHost // com.compose.sdl.window/LocalPopupHost|{}LocalPopupHost[0]
|
||||||
final fun <get-LocalPopupHost>(): androidx.compose.runtime/ProvidableCompositionLocal<com.compose.sdl.window/PopupHostState> // com.compose.sdl.window/LocalPopupHost.<get-LocalPopupHost>|<get-LocalPopupHost>(){}[0]
|
final fun <get-LocalPopupHost>(): androidx.compose.runtime/ProvidableCompositionLocal<com.compose.sdl.window/PopupHostState> // com.compose.sdl.window/LocalPopupHost.<get-LocalPopupHost>|<get-LocalPopupHost>(){}[0]
|
||||||
final val com.compose.sdl.window/com_compose_sdl_window_PopupExitHandle$stableprop // com.compose.sdl.window/com_compose_sdl_window_PopupExitHandle$stableprop|#static{}com_compose_sdl_window_PopupExitHandle$stableprop[0]
|
|
||||||
final val com.compose.sdl.window/com_compose_sdl_window_PopupHostState$stableprop // com.compose.sdl.window/com_compose_sdl_window_PopupHostState$stableprop|#static{}com_compose_sdl_window_PopupHostState$stableprop[0]
|
final val com.compose.sdl.window/com_compose_sdl_window_PopupHostState$stableprop // com.compose.sdl.window/com_compose_sdl_window_PopupHostState$stableprop|#static{}com_compose_sdl_window_PopupHostState$stableprop[0]
|
||||||
final val com.compose.sdl/LocalComposeNativeWindow // com.compose.sdl/LocalComposeNativeWindow|{}LocalComposeNativeWindow[0]
|
final val com.compose.sdl/LocalComposeNativeWindow // com.compose.sdl/LocalComposeNativeWindow|{}LocalComposeNativeWindow[0]
|
||||||
final fun <get-LocalComposeNativeWindow>(): androidx.compose.runtime/ProvidableCompositionLocal<com.compose.sdl/ComposeNativeWindow> // com.compose.sdl/LocalComposeNativeWindow.<get-LocalComposeNativeWindow>|<get-LocalComposeNativeWindow>(){}[0]
|
final fun <get-LocalComposeNativeWindow>(): androidx.compose.runtime/ProvidableCompositionLocal<com.compose.sdl/ComposeNativeWindow> // com.compose.sdl/LocalComposeNativeWindow.<get-LocalComposeNativeWindow>|<get-LocalComposeNativeWindow>(){}[0]
|
||||||
@@ -4890,11 +4859,11 @@ final val com.compose.sdl/com_compose_sdl_AppEvent_WindowResized$stableprop // c
|
|||||||
final val com.compose.sdl/com_compose_sdl_ComposeNativeWindow$stableprop // com.compose.sdl/com_compose_sdl_ComposeNativeWindow$stableprop|#static{}com_compose_sdl_ComposeNativeWindow$stableprop[0]
|
final val com.compose.sdl/com_compose_sdl_ComposeNativeWindow$stableprop // com.compose.sdl/com_compose_sdl_ComposeNativeWindow$stableprop|#static{}com_compose_sdl_ComposeNativeWindow$stableprop[0]
|
||||||
final val com.compose.sdl/com_compose_sdl_GpuMode$stableprop // com.compose.sdl/com_compose_sdl_GpuMode$stableprop|#static{}com_compose_sdl_GpuMode$stableprop[0]
|
final val com.compose.sdl/com_compose_sdl_GpuMode$stableprop // com.compose.sdl/com_compose_sdl_GpuMode$stableprop|#static{}com_compose_sdl_GpuMode$stableprop[0]
|
||||||
final val com.compose.sdl/com_compose_sdl_GpuMode_Auto$stableprop // com.compose.sdl/com_compose_sdl_GpuMode_Auto$stableprop|#static{}com_compose_sdl_GpuMode_Auto$stableprop[0]
|
final val com.compose.sdl/com_compose_sdl_GpuMode_Auto$stableprop // com.compose.sdl/com_compose_sdl_GpuMode_Auto$stableprop|#static{}com_compose_sdl_GpuMode_Auto$stableprop[0]
|
||||||
final val com.compose.sdl/com_compose_sdl_GpuMode_Skia$stableprop // com.compose.sdl/com_compose_sdl_GpuMode_Skia$stableprop|#static{}com_compose_sdl_GpuMode_Skia$stableprop[0]
|
final val com.compose.sdl/com_compose_sdl_GpuMode_CpuRaster$stableprop // com.compose.sdl/com_compose_sdl_GpuMode_CpuRaster$stableprop|#static{}com_compose_sdl_GpuMode_CpuRaster$stableprop[0]
|
||||||
final val com.compose.sdl/com_compose_sdl_GpuMode_Skia_Metal$stableprop // com.compose.sdl/com_compose_sdl_GpuMode_Skia_Metal$stableprop|#static{}com_compose_sdl_GpuMode_Skia_Metal$stableprop[0]
|
final val com.compose.sdl/com_compose_sdl_GpuMode_Metal$stableprop // com.compose.sdl/com_compose_sdl_GpuMode_Metal$stableprop|#static{}com_compose_sdl_GpuMode_Metal$stableprop[0]
|
||||||
final val com.compose.sdl/com_compose_sdl_GpuMode_Skia_OpenGL$stableprop // com.compose.sdl/com_compose_sdl_GpuMode_Skia_OpenGL$stableprop|#static{}com_compose_sdl_GpuMode_Skia_OpenGL$stableprop[0]
|
final val com.compose.sdl/com_compose_sdl_GpuMode_OpenGL$stableprop // com.compose.sdl/com_compose_sdl_GpuMode_OpenGL$stableprop|#static{}com_compose_sdl_GpuMode_OpenGL$stableprop[0]
|
||||||
final val com.compose.sdl/com_compose_sdl_GpuMode_Software$stableprop // com.compose.sdl/com_compose_sdl_GpuMode_Software$stableprop|#static{}com_compose_sdl_GpuMode_Software$stableprop[0]
|
final val com.compose.sdl/com_compose_sdl_RawSdlHandles$stableprop // com.compose.sdl/com_compose_sdl_RawSdlHandles$stableprop|#static{}com_compose_sdl_RawSdlHandles$stableprop[0]
|
||||||
final val com.compose.sdl/com_compose_sdl_SDL3Backend$stableprop // com.compose.sdl/com_compose_sdl_SDL3Backend$stableprop|#static{}com_compose_sdl_SDL3Backend$stableprop[0]
|
final val com.compose.sdl/com_compose_sdl_Sdl3Backend$stableprop // com.compose.sdl/com_compose_sdl_Sdl3Backend$stableprop|#static{}com_compose_sdl_Sdl3Backend$stableprop[0]
|
||||||
final val com.compose.sdl/com_compose_sdl_TextLayoutConfig$stableprop // com.compose.sdl/com_compose_sdl_TextLayoutConfig$stableprop|#static{}com_compose_sdl_TextLayoutConfig$stableprop[0]
|
final val com.compose.sdl/com_compose_sdl_TextLayoutConfig$stableprop // com.compose.sdl/com_compose_sdl_TextLayoutConfig$stableprop|#static{}com_compose_sdl_TextLayoutConfig$stableprop[0]
|
||||||
|
|
||||||
final var androidx.compose.ui.platform/isDebugInspectorInfoEnabled // androidx.compose.ui.platform/isDebugInspectorInfoEnabled|{}isDebugInspectorInfoEnabled[0]
|
final var androidx.compose.ui.platform/isDebugInspectorInfoEnabled // androidx.compose.ui.platform/isDebugInspectorInfoEnabled|{}isDebugInspectorInfoEnabled[0]
|
||||||
@@ -5389,16 +5358,10 @@ final fun androidx.compose.ui/androidx_compose_ui_CombinedModifier$stableprop_ge
|
|||||||
final fun androidx.compose.ui/androidx_compose_ui_ComposeUiFlags$stableprop_getter(): kotlin/Int // androidx.compose.ui/androidx_compose_ui_ComposeUiFlags$stableprop_getter|androidx_compose_ui_ComposeUiFlags$stableprop_getter(){}[0]
|
final fun androidx.compose.ui/androidx_compose_ui_ComposeUiFlags$stableprop_getter(): kotlin/Int // androidx.compose.ui/androidx_compose_ui_ComposeUiFlags$stableprop_getter|androidx_compose_ui_ComposeUiFlags$stableprop_getter(){}[0]
|
||||||
final fun androidx.compose.ui/androidx_compose_ui_Modifier_Node$stableprop_getter(): kotlin/Int // androidx.compose.ui/androidx_compose_ui_Modifier_Node$stableprop_getter|androidx_compose_ui_Modifier_Node$stableprop_getter(){}[0]
|
final fun androidx.compose.ui/androidx_compose_ui_Modifier_Node$stableprop_getter(): kotlin/Int // androidx.compose.ui/androidx_compose_ui_Modifier_Node$stableprop_getter|androidx_compose_ui_Modifier_Node$stableprop_getter(){}[0]
|
||||||
final fun androidx.compose.ui/isUiSystemInDarkTheme(androidx.compose.runtime/Composer?, kotlin/Int): kotlin/Boolean // androidx.compose.ui/isUiSystemInDarkTheme|isUiSystemInDarkTheme(androidx.compose.runtime.Composer?;kotlin.Int){}[0]
|
final fun androidx.compose.ui/isUiSystemInDarkTheme(androidx.compose.runtime/Composer?, kotlin/Int): kotlin/Boolean // androidx.compose.ui/isUiSystemInDarkTheme|isUiSystemInDarkTheme(androidx.compose.runtime.Composer?;kotlin.Int){}[0]
|
||||||
final fun com.compose.sdl.input/com_compose_sdl_input_LegacyPointerEvent$stableprop_getter(): kotlin/Int // com.compose.sdl.input/com_compose_sdl_input_LegacyPointerEvent$stableprop_getter|com_compose_sdl_input_LegacyPointerEvent$stableprop_getter(){}[0]
|
|
||||||
final fun com.compose.sdl.node/com_compose_sdl_node_ComposeRootHost$stableprop_getter(): kotlin/Int // com.compose.sdl.node/com_compose_sdl_node_ComposeRootHost$stableprop_getter|com_compose_sdl_node_ComposeRootHost$stableprop_getter(){}[0]
|
final fun com.compose.sdl.node/com_compose_sdl_node_ComposeRootHost$stableprop_getter(): kotlin/Int // com.compose.sdl.node/com_compose_sdl_node_ComposeRootHost$stableprop_getter|com_compose_sdl_node_ComposeRootHost$stableprop_getter(){}[0]
|
||||||
final fun com.compose.sdl.platform/com_compose_sdl_platform_DefaultViewConfiguration$stableprop_getter(): kotlin/Int // com.compose.sdl.platform/com_compose_sdl_platform_DefaultViewConfiguration$stableprop_getter|com_compose_sdl_platform_DefaultViewConfiguration$stableprop_getter(){}[0]
|
|
||||||
final fun com.compose.sdl.renderer.skia/com_compose_sdl_renderer_skia_SkiaSurfaceBridge$stableprop_getter(): kotlin/Int // com.compose.sdl.renderer.skia/com_compose_sdl_renderer_skia_SkiaSurfaceBridge$stableprop_getter|com_compose_sdl_renderer_skia_SkiaSurfaceBridge$stableprop_getter(){}[0]
|
|
||||||
final fun com.compose.sdl.text.input/com_compose_sdl_text_input_ImeBridge$stableprop_getter(): kotlin/Int // com.compose.sdl.text.input/com_compose_sdl_text_input_ImeBridge$stableprop_getter|com_compose_sdl_text_input_ImeBridge$stableprop_getter(){}[0]
|
final fun com.compose.sdl.text.input/com_compose_sdl_text_input_ImeBridge$stableprop_getter(): kotlin/Int // com.compose.sdl.text.input/com_compose_sdl_text_input_ImeBridge$stableprop_getter|com_compose_sdl_text_input_ImeBridge$stableprop_getter(){}[0]
|
||||||
final fun com.compose.sdl.text/registerGenericFonts() // com.compose.sdl.text/registerGenericFonts|registerGenericFonts(){}[0]
|
final fun com.compose.sdl.text/registerGenericFonts() // com.compose.sdl.text/registerGenericFonts|registerGenericFonts(){}[0]
|
||||||
final fun com.compose.sdl.window/PopupLayer(com.compose.sdl.window/PopupHostState, androidx.compose.runtime/Composer?, kotlin/Int) // com.compose.sdl.window/PopupLayer|PopupLayer(com.compose.sdl.window.PopupHostState;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
|
final fun com.compose.sdl.window/PopupLayer(com.compose.sdl.window/PopupHostState, androidx.compose.runtime/Composer?, kotlin/Int) // com.compose.sdl.window/PopupLayer|PopupLayer(com.compose.sdl.window.PopupHostState;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
|
||||||
final fun com.compose.sdl.window/PopupOutsideDismiss(kotlin/Int, kotlin/Int, kotlin/Int, kotlin/Int, kotlin/Function0<kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int) // com.compose.sdl.window/PopupOutsideDismiss|PopupOutsideDismiss(kotlin.Int;kotlin.Int;kotlin.Int;kotlin.Int;kotlin.Function0<kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
|
|
||||||
final fun com.compose.sdl.window/PositionedPopup(kotlin/Int, kotlin/Int, kotlin/Function0<kotlin/Unit>, kotlin/Function2<androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int) // com.compose.sdl.window/PositionedPopup|PositionedPopup(kotlin.Int;kotlin.Int;kotlin.Function0<kotlin.Unit>;kotlin.Function2<androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
|
|
||||||
final fun com.compose.sdl.window/com_compose_sdl_window_PopupExitHandle$stableprop_getter(): kotlin/Int // com.compose.sdl.window/com_compose_sdl_window_PopupExitHandle$stableprop_getter|com_compose_sdl_window_PopupExitHandle$stableprop_getter(){}[0]
|
|
||||||
final fun com.compose.sdl.window/com_compose_sdl_window_PopupHostState$stableprop_getter(): kotlin/Int // com.compose.sdl.window/com_compose_sdl_window_PopupHostState$stableprop_getter|com_compose_sdl_window_PopupHostState$stableprop_getter(){}[0]
|
final fun com.compose.sdl.window/com_compose_sdl_window_PopupHostState$stableprop_getter(): kotlin/Int // com.compose.sdl.window/com_compose_sdl_window_PopupHostState$stableprop_getter|com_compose_sdl_window_PopupHostState$stableprop_getter(){}[0]
|
||||||
final fun com.compose.sdl.window/createPopupHostState(): com.compose.sdl.window/PopupHostState // com.compose.sdl.window/createPopupHostState|createPopupHostState(){}[0]
|
final fun com.compose.sdl.window/createPopupHostState(): com.compose.sdl.window/PopupHostState // com.compose.sdl.window/createPopupHostState|createPopupHostState(){}[0]
|
||||||
final fun com.compose.sdl/appDataDir(kotlin/String, kotlin/String): kotlin/String? // com.compose.sdl/appDataDir|appDataDir(kotlin.String;kotlin.String){}[0]
|
final fun com.compose.sdl/appDataDir(kotlin/String, kotlin/String): kotlin/String? // com.compose.sdl/appDataDir|appDataDir(kotlin.String;kotlin.String){}[0]
|
||||||
@@ -5420,13 +5383,13 @@ final fun com.compose.sdl/com_compose_sdl_AppEvent_WindowResized$stableprop_gett
|
|||||||
final fun com.compose.sdl/com_compose_sdl_ComposeNativeWindow$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_ComposeNativeWindow$stableprop_getter|com_compose_sdl_ComposeNativeWindow$stableprop_getter(){}[0]
|
final fun com.compose.sdl/com_compose_sdl_ComposeNativeWindow$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_ComposeNativeWindow$stableprop_getter|com_compose_sdl_ComposeNativeWindow$stableprop_getter(){}[0]
|
||||||
final fun com.compose.sdl/com_compose_sdl_GpuMode$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_GpuMode$stableprop_getter|com_compose_sdl_GpuMode$stableprop_getter(){}[0]
|
final fun com.compose.sdl/com_compose_sdl_GpuMode$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_GpuMode$stableprop_getter|com_compose_sdl_GpuMode$stableprop_getter(){}[0]
|
||||||
final fun com.compose.sdl/com_compose_sdl_GpuMode_Auto$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_GpuMode_Auto$stableprop_getter|com_compose_sdl_GpuMode_Auto$stableprop_getter(){}[0]
|
final fun com.compose.sdl/com_compose_sdl_GpuMode_Auto$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_GpuMode_Auto$stableprop_getter|com_compose_sdl_GpuMode_Auto$stableprop_getter(){}[0]
|
||||||
final fun com.compose.sdl/com_compose_sdl_GpuMode_Skia$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_GpuMode_Skia$stableprop_getter|com_compose_sdl_GpuMode_Skia$stableprop_getter(){}[0]
|
final fun com.compose.sdl/com_compose_sdl_GpuMode_CpuRaster$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_GpuMode_CpuRaster$stableprop_getter|com_compose_sdl_GpuMode_CpuRaster$stableprop_getter(){}[0]
|
||||||
final fun com.compose.sdl/com_compose_sdl_GpuMode_Skia_Metal$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_GpuMode_Skia_Metal$stableprop_getter|com_compose_sdl_GpuMode_Skia_Metal$stableprop_getter(){}[0]
|
final fun com.compose.sdl/com_compose_sdl_GpuMode_Metal$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_GpuMode_Metal$stableprop_getter|com_compose_sdl_GpuMode_Metal$stableprop_getter(){}[0]
|
||||||
final fun com.compose.sdl/com_compose_sdl_GpuMode_Skia_OpenGL$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_GpuMode_Skia_OpenGL$stableprop_getter|com_compose_sdl_GpuMode_Skia_OpenGL$stableprop_getter(){}[0]
|
final fun com.compose.sdl/com_compose_sdl_GpuMode_OpenGL$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_GpuMode_OpenGL$stableprop_getter|com_compose_sdl_GpuMode_OpenGL$stableprop_getter(){}[0]
|
||||||
final fun com.compose.sdl/com_compose_sdl_GpuMode_Software$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_GpuMode_Software$stableprop_getter|com_compose_sdl_GpuMode_Software$stableprop_getter(){}[0]
|
final fun com.compose.sdl/com_compose_sdl_RawSdlHandles$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_RawSdlHandles$stableprop_getter|com_compose_sdl_RawSdlHandles$stableprop_getter(){}[0]
|
||||||
final fun com.compose.sdl/com_compose_sdl_SDL3Backend$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_SDL3Backend$stableprop_getter|com_compose_sdl_SDL3Backend$stableprop_getter(){}[0]
|
final fun com.compose.sdl/com_compose_sdl_Sdl3Backend$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_Sdl3Backend$stableprop_getter|com_compose_sdl_Sdl3Backend$stableprop_getter(){}[0]
|
||||||
final fun com.compose.sdl/com_compose_sdl_TextLayoutConfig$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_TextLayoutConfig$stableprop_getter|com_compose_sdl_TextLayoutConfig$stableprop_getter(){}[0]
|
final fun com.compose.sdl/com_compose_sdl_TextLayoutConfig$stableprop_getter(): kotlin/Int // com.compose.sdl/com_compose_sdl_TextLayoutConfig$stableprop_getter|com_compose_sdl_TextLayoutConfig$stableprop_getter(){}[0]
|
||||||
final fun com.compose.sdl/createRenderBackend(com.compose.sdl/SDL3Backend, com.compose.sdl/GpuMode): com.compose.sdl/RenderBackend? // com.compose.sdl/createRenderBackend|createRenderBackend(com.compose.sdl.SDL3Backend;com.compose.sdl.GpuMode){}[0]
|
final fun com.compose.sdl/createRenderBackend(com.compose.sdl/Sdl3Backend, com.compose.sdl/GpuMode): com.compose.sdl/RenderBackend? // com.compose.sdl/createRenderBackend|createRenderBackend(com.compose.sdl.Sdl3Backend;com.compose.sdl.GpuMode){}[0]
|
||||||
final fun com.compose.sdl/fileManagerName(): kotlin/String // com.compose.sdl/fileManagerName|fileManagerName(){}[0]
|
final fun com.compose.sdl/fileManagerName(): kotlin/String // com.compose.sdl/fileManagerName|fileManagerName(){}[0]
|
||||||
final fun com.compose.sdl/hasComposeResource(kotlin/String): kotlin/Boolean // com.compose.sdl/hasComposeResource|hasComposeResource(kotlin.String){}[0]
|
final fun com.compose.sdl/hasComposeResource(kotlin/String): kotlin/Boolean // com.compose.sdl/hasComposeResource|hasComposeResource(kotlin.String){}[0]
|
||||||
final fun com.compose.sdl/injectKey(kotlin/Int, kotlin/Boolean) // com.compose.sdl/injectKey|injectKey(kotlin.Int;kotlin.Boolean){}[0]
|
final fun com.compose.sdl/injectKey(kotlin/Int, kotlin/Boolean) // com.compose.sdl/injectKey|injectKey(kotlin.Int;kotlin.Boolean){}[0]
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import androidx.compose.ui.unit.DpSize
|
|||||||
* StubOwner). Constant values match the upstream `PlatformContext.DefaultViewConfiguration`
|
* StubOwner). Constant values match the upstream `PlatformContext.DefaultViewConfiguration`
|
||||||
* (longPressTimeout=500ms, doubleTapTimeout=300ms, doubleTapMinTime=40ms, touchSlop=8f).
|
* (longPressTimeout=500ms, doubleTapTimeout=300ms, doubleTapMinTime=40ms, touchSlop=8f).
|
||||||
*/
|
*/
|
||||||
object DefaultViewConfiguration : ViewConfiguration {
|
internal object DefaultViewConfiguration : ViewConfiguration {
|
||||||
override val longPressTimeoutMillis: Long = 500
|
override val longPressTimeoutMillis: Long = 500
|
||||||
override val doubleTapTimeoutMillis: Long = 300
|
override val doubleTapTimeoutMillis: Long = 300
|
||||||
override val doubleTapMinTimeMillis: Long = 40
|
override val doubleTapMinTimeMillis: Long = 40
|
||||||
|
|||||||
@@ -98,23 +98,23 @@ class PopupHostState internal constructor() {
|
|||||||
fun contains(inX: Int, inY: Int) = inX >= x && inX < x + w && inY >= y && inY < y + h
|
fun contains(inX: Int, inY: Int) = inX >= x && inX < x + w && inY >= y && inY < y + h
|
||||||
}
|
}
|
||||||
|
|
||||||
private val fDismissers = mutableListOf<Dismisser>()
|
private val mDismissers = mutableListOf<Dismisser>()
|
||||||
|
|
||||||
internal fun setDismisser(inId: Any, inX: Int, inY: Int, inW: Int, inH: Int, inOnDismiss: () -> Unit) {
|
internal fun setDismisser(inId: Any, inX: Int, inY: Int, inW: Int, inH: Int, inOnDismiss: () -> Unit) {
|
||||||
val vD = fDismissers.firstOrNull { it.id === inId } ?: Dismisser(inId).also { fDismissers.add(it) }
|
val vD = mDismissers.firstOrNull { it.id === inId } ?: Dismisser(inId).also { mDismissers.add(it) }
|
||||||
vD.x = inX; vD.y = inY; vD.w = inW; vD.h = inH; vD.onDismiss = inOnDismiss
|
vD.x = inX; vD.y = inY; vD.w = inW; vD.h = inH; vD.onDismiss = inOnDismiss
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun removeDismisser(inId: Any) {
|
internal fun removeDismisser(inId: Any) {
|
||||||
fDismissers.removeAll { it.id === inId }
|
mDismissers.removeAll { it.id === inId }
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Dismiss every registered popup whose content rect does NOT contain the
|
/** Dismiss every registered popup whose content rect does NOT contain the
|
||||||
press. Called from the window's press dispatch; never consumes the press. */
|
press. Called from the window's press dispatch; never consumes the press. */
|
||||||
fun notifyOutsidePress(inX: Int, inY: Int) {
|
fun notifyOutsidePress(inX: Int, inY: Int) {
|
||||||
if (fDismissers.isEmpty()) return
|
if (mDismissers.isEmpty()) return
|
||||||
// Copy: an onDismiss typically removes the dismisser (state write).
|
// Copy: an onDismiss typically removes the dismisser (state write).
|
||||||
for (vD in fDismissers.toList()) {
|
for (vD in mDismissers.toList()) {
|
||||||
if (vD.w > 0 && vD.h > 0 && !vD.contains(inX, inY)) vD.onDismiss()
|
if (vD.w > 0 && vD.h > 0 && !vD.contains(inX, inY)) vD.onDismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -139,26 +139,26 @@ fun createPopupHostState(): PopupHostState = PopupHostState()
|
|||||||
the entry. The hosted content (still composed - it lives in the HOST's
|
the entry. The hosted content (still composed - it lives in the HOST's
|
||||||
composition, not the owner's) observes that, plays its animation, and MUST
|
composition, not the owner's) observes that, plays its animation, and MUST
|
||||||
end with [finish], which actually removes the entry. */
|
end with [finish], which actually removes the entry. */
|
||||||
class PopupExitHandle internal constructor(
|
internal class PopupExitHandle internal constructor(
|
||||||
private val fHost: PopupHostState, // owning host - target of finish()
|
private val mHost: PopupHostState, // owning host - target of finish()
|
||||||
private val fEntry: PopupHostState.Entry, // the entry this handle controls
|
private val mEntry: PopupHostState.Entry, // the entry this handle controls
|
||||||
) {
|
) {
|
||||||
/** True once the owning Popup left the composition and the host is waiting
|
/** True once the owning Popup left the composition and the host is waiting
|
||||||
for this entry's exit animation. Observable - recomposes the content. */
|
for this entry's exit animation. Observable - recomposes the content. */
|
||||||
val isExiting: State<Boolean> get() = fEntry.exiting
|
val isExiting: State<Boolean> get() = mEntry.exiting
|
||||||
|
|
||||||
/** Opt in to exit deferral. Idempotent; call from a SideEffect. */
|
/** Opt in to exit deferral. Idempotent; call from a SideEffect. */
|
||||||
fun enableExitTransition() { fEntry.hasExitTransition = true }
|
fun enableExitTransition() { mEntry.hasExitTransition = true }
|
||||||
|
|
||||||
/** Ends the deferral - the entry is removed from the host for real. */
|
/** Ends the deferral - the entry is removed from the host for real. */
|
||||||
fun finish() { fHost.forceRemove(fEntry.id) }
|
fun finish() { mHost.forceRemove(mEntry.id) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Per-entry handle, provided by PopupLayer around each hosted content. Null
|
/** Per-entry handle, provided by PopupLayer around each hosted content. Null
|
||||||
outside a popup layer. NB: safe to expose through the caller-locals rewrap
|
outside a popup layer. NB: safe to expose through the caller-locals rewrap
|
||||||
in Popup (CompositionLocalProvider(callerContext)) - the caller never
|
in Popup (CompositionLocalProvider(callerContext)) - the caller never
|
||||||
provides this local, so the layer's per-entry value stays visible. */
|
provides this local, so the layer's per-entry value stays visible. */
|
||||||
val LocalPopupExitHandle = staticCompositionLocalOf<PopupExitHandle?> { null }
|
internal val LocalPopupExitHandle = staticCompositionLocalOf<PopupExitHandle?> { null }
|
||||||
|
|
||||||
// ==================
|
// ==================
|
||||||
// MARK: PopupLayer
|
// MARK: PopupLayer
|
||||||
@@ -187,7 +187,7 @@ fun PopupLayer(inHost: PopupHostState) {
|
|||||||
popup host so the dismissing press is NOT consumed (it still reaches whatever
|
popup host so the dismissing press is NOT consumed (it still reaches whatever
|
||||||
is under it - no dead first click). */
|
is under it - no dead first click). */
|
||||||
@Composable
|
@Composable
|
||||||
fun PopupOutsideDismiss(inX: Int, inY: Int, inW: Int, inH: Int, onDismissRequest: () -> Unit) {
|
internal fun PopupOutsideDismiss(inX: Int, inY: Int, inW: Int, inH: Int, onDismissRequest: () -> Unit) {
|
||||||
val vHost = LocalPopupHost.current
|
val vHost = LocalPopupHost.current
|
||||||
val vId = remember { Any() }
|
val vId = remember { Any() }
|
||||||
SideEffect { vHost.setDismisser(vId, inX, inY, inW, inH, onDismissRequest) }
|
SideEffect { vHost.setDismisser(vId, inX, inY, inW, inH, onDismissRequest) }
|
||||||
@@ -203,7 +203,7 @@ fun PopupOutsideDismiss(inX: Int, inY: Int, inW: Int, inH: Int, onDismissRequest
|
|||||||
`x`/`y` are LAYOUT PIXELS (matches `LayoutCoordinates.positionInRoot`), not
|
`x`/`y` are LAYOUT PIXELS (matches `LayoutCoordinates.positionInRoot`), not
|
||||||
`Dp` - layout runs in physical pixels under the Option-B density flow. */
|
`Dp` - layout runs in physical pixels under the Option-B density flow. */
|
||||||
@Composable
|
@Composable
|
||||||
fun PositionedPopup(
|
internal fun PositionedPopup(
|
||||||
x: Int,
|
x: Int,
|
||||||
y: Int,
|
y: Int,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@ import androidx.compose.ui.geometry.Offset
|
|||||||
|
|
||||||
/* Actuals for the vendored `DragAndDrop.kt` expect classes.
|
/* Actuals for the vendored `DragAndDrop.kt` expect classes.
|
||||||
* Backed by SDL3's SDL_EVENT_DROP_* stream (file / text / begin / position /
|
* Backed by SDL3's SDL_EVENT_DROP_* stream (file / text / begin / position /
|
||||||
* complete) - see Sdl3DragAndDropOwner + SDL3EventMapper for the pump side.
|
* complete) - see Sdl3DragAndDropOwner + AppEvent.kt for the pump side.
|
||||||
*
|
*
|
||||||
* DragAndDropTransferData carries the payload a drop into the window
|
* DragAndDropTransferData carries the payload a drop into the window
|
||||||
* delivered: zero or more file paths (SDL_EVENT_DROP_FILE) plus an optional
|
* delivered: zero or more file paths (SDL_EVENT_DROP_FILE) plus an optional
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@ import androidx.compose.ui.node.ModifierNodeElement
|
|||||||
|
|
||||||
/** The per-window DragAndDropManager backing ComposeOwner. Holds a root
|
/** The per-window DragAndDropManager backing ComposeOwner. Holds a root
|
||||||
* DragAndDropNode installed on the root LayoutNode via [modifier]; the SDL
|
* DragAndDropNode installed on the root LayoutNode via [modifier]; the SDL
|
||||||
* event pump (see SDL3EventMapper + ComposeWindow) drives [handleDrop] which
|
* event pump (see AppEvent.kt + ComposeWindow) drives [handleDrop] which
|
||||||
* accumulates BEGIN → FILE / TEXT / POSITION → COMPLETE into a single
|
* accumulates BEGIN → FILE / TEXT / POSITION → COMPLETE into a single
|
||||||
* DragAndDropEvent and dispatches through the node tree.
|
* DragAndDropEvent and dispatches through the node tree.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ package androidx.compose.ui.input.key
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Project [Key] actual - assigns a unique sequential Long to each named
|
* Project [Key] actual - assigns a unique sequential Long to each named
|
||||||
* key. SDL3EventMapper picks the right `Key.X` by switch on SDL scancode
|
* key. The SDL event mapper picks the right `Key.X` by switch on SDL scancode
|
||||||
* (`kKeyForScancode` in `SDL3EventMapper.kt`). Comparison works because
|
* (`kKeyForScancode` in `AppEvent.kt`). Comparison works because
|
||||||
* Key is a value class - `Key(42L) == Key(42L)`.
|
* Key is a value class - `Key(42L) == Key(42L)`.
|
||||||
*/
|
*/
|
||||||
@kotlin.jvm.JvmInline
|
@kotlin.jvm.JvmInline
|
||||||
|
|||||||
+17
-17
@@ -103,14 +103,14 @@ private const val kImageMime = "image/png"
|
|||||||
// call with an image). Held on nativeHeap because SDL keeps the callback alive
|
// call with an image). Held on nativeHeap because SDL keeps the callback alive
|
||||||
// until the clipboard is cleared or another SDL_SetClipboardData replaces us -
|
// until the clipboard is cleared or another SDL_SetClipboardData replaces us -
|
||||||
// a Kotlin heap reference would break as soon as the caller's coroutine
|
// a Kotlin heap reference would break as soon as the caller's coroutine
|
||||||
// returned. Read by [fClipboardDataCallback], freed by [fClipboardCleanupCallback].
|
// returned. Read by [mClipboardDataCallback], freed by [mClipboardCleanupCallback].
|
||||||
private var fOutgoingImagePtr: CPointer<ByteVar>? = null
|
private var mOutgoingImagePtr: CPointer<ByteVar>? = null
|
||||||
private var fOutgoingImageSize: Int = 0
|
private var mOutgoingImageSize: Int = 0
|
||||||
|
|
||||||
// SDL fires this on the main event thread when another app asks the clipboard
|
// SDL fires this on the main event thread when another app asks the clipboard
|
||||||
// for a MIME we advertised. We serve back the pointer + size we stashed at
|
// for a MIME we advertised. We serve back the pointer + size we stashed at
|
||||||
// SDL_SetClipboardData time. Return NULL if the MIME doesn't match.
|
// SDL_SetClipboardData time. Return NULL if the MIME doesn't match.
|
||||||
private val fClipboardDataCallback = staticCFunction {
|
private val mClipboardDataCallback = staticCFunction {
|
||||||
userdata: COpaquePointer?,
|
userdata: COpaquePointer?,
|
||||||
mimeType: CPointer<ByteVar>?,
|
mimeType: CPointer<ByteVar>?,
|
||||||
sizePtr: CPointer<platform.posix.size_tVar>?,
|
sizePtr: CPointer<platform.posix.size_tVar>?,
|
||||||
@@ -118,20 +118,20 @@ private val fClipboardDataCallback = staticCFunction {
|
|||||||
userdata.hashCode() // unused
|
userdata.hashCode() // unused
|
||||||
val vRequested = mimeType?.toKString() ?: return@staticCFunction null
|
val vRequested = mimeType?.toKString() ?: return@staticCFunction null
|
||||||
if (vRequested != kImageMime) return@staticCFunction null
|
if (vRequested != kImageMime) return@staticCFunction null
|
||||||
val vPtr = fOutgoingImagePtr ?: return@staticCFunction null
|
val vPtr = mOutgoingImagePtr ?: return@staticCFunction null
|
||||||
sizePtr?.pointed?.value = fOutgoingImageSize.toULong()
|
sizePtr?.pointed?.value = mOutgoingImageSize.toULong()
|
||||||
vPtr as COpaquePointer
|
vPtr as COpaquePointer
|
||||||
}
|
}
|
||||||
|
|
||||||
// SDL calls this when the clipboard we set is cleared or overwritten (by us
|
// SDL calls this when the clipboard we set is cleared or overwritten (by us
|
||||||
// or another app). Frees the nativeHeap allocation.
|
// or another app). Frees the nativeHeap allocation.
|
||||||
private val fClipboardCleanupCallback = staticCFunction {
|
private val mClipboardCleanupCallback = staticCFunction {
|
||||||
userdata: COpaquePointer?,
|
userdata: COpaquePointer?,
|
||||||
->
|
->
|
||||||
userdata.hashCode() // unused
|
userdata.hashCode() // unused
|
||||||
fOutgoingImagePtr?.let { nativeHeap.free(it.rawValue) }
|
mOutgoingImagePtr?.let { nativeHeap.free(it.rawValue) }
|
||||||
fOutgoingImagePtr = null
|
mOutgoingImagePtr = null
|
||||||
fOutgoingImageSize = 0
|
mOutgoingImageSize = 0
|
||||||
Unit
|
Unit
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,19 +172,19 @@ private fun sdlReadImage(): ByteArray? = memScoped {
|
|||||||
private fun sdlWriteImage(bytes: ByteArray) {
|
private fun sdlWriteImage(bytes: ByteArray) {
|
||||||
// Free any previously-held buffer proactively (the cleanup callback also
|
// Free any previously-held buffer proactively (the cleanup callback also
|
||||||
// fires from SDL, but doing it eagerly here handles same-thread re-set).
|
// fires from SDL, but doing it eagerly here handles same-thread re-set).
|
||||||
fOutgoingImagePtr?.let { nativeHeap.free(it.rawValue) }
|
mOutgoingImagePtr?.let { nativeHeap.free(it.rawValue) }
|
||||||
|
|
||||||
val vPtr = nativeHeap.allocArray<ByteVar>(bytes.size)
|
val vPtr = nativeHeap.allocArray<ByteVar>(bytes.size)
|
||||||
for (i in bytes.indices) vPtr[i] = bytes[i]
|
for (i in bytes.indices) vPtr[i] = bytes[i]
|
||||||
fOutgoingImagePtr = vPtr
|
mOutgoingImagePtr = vPtr
|
||||||
fOutgoingImageSize = bytes.size
|
mOutgoingImageSize = bytes.size
|
||||||
|
|
||||||
memScoped {
|
memScoped {
|
||||||
val vMimeArray = allocArray<CPointerVar<ByteVar>>(1)
|
val vMimeArray = allocArray<CPointerVar<ByteVar>>(1)
|
||||||
vMimeArray[0] = kImageMime.cstr.ptr
|
vMimeArray[0] = kImageMime.cstr.ptr
|
||||||
SDL_SetClipboardData(
|
SDL_SetClipboardData(
|
||||||
fClipboardDataCallback,
|
mClipboardDataCallback,
|
||||||
fClipboardCleanupCallback,
|
mClipboardCleanupCallback,
|
||||||
null,
|
null,
|
||||||
vMimeArray,
|
vMimeArray,
|
||||||
1.toULong(),
|
1.toULong(),
|
||||||
@@ -213,10 +213,10 @@ private fun sdlWriteEntry(entry: ClipEntry?) {
|
|||||||
// The SDL3-backed Clipboard implementation. Singleton because SDL's clipboard
|
// The SDL3-backed Clipboard implementation. Singleton because SDL's clipboard
|
||||||
// is process-global - allocating a new one per Window() would only waste
|
// is process-global - allocating a new one per Window() would only waste
|
||||||
// memory. Consumed by :desktop-native-window when seeding LocalClipboard.
|
// memory. Consumed by :desktop-native-window when seeding LocalClipboard.
|
||||||
private object SDL3Clipboard : Clipboard {
|
private object Sdl3Clipboard : Clipboard {
|
||||||
override suspend fun getClipEntry(): ClipEntry? = sdlReadEntry()
|
override suspend fun getClipEntry(): ClipEntry? = sdlReadEntry()
|
||||||
override suspend fun setClipEntry(clipEntry: ClipEntry?) { sdlWriteEntry(clipEntry) }
|
override suspend fun setClipEntry(clipEntry: ClipEntry?) { sdlWriteEntry(clipEntry) }
|
||||||
override val nativeClipboard: NativeClipboard get() = nativeClipboardSentinel
|
override val nativeClipboard: NativeClipboard get() = nativeClipboardSentinel
|
||||||
}
|
}
|
||||||
|
|
||||||
fun platformClipboard(): Clipboard = SDL3Clipboard
|
fun platformClipboard(): Clipboard = Sdl3Clipboard
|
||||||
|
|||||||
+4
-4
@@ -18,12 +18,12 @@ import androidx.compose.ui.unit.IntRect
|
|||||||
* no-ops.
|
* no-ops.
|
||||||
*/
|
*/
|
||||||
private class StubSemanticsRegion : SemanticsRegion {
|
private class StubSemanticsRegion : SemanticsRegion {
|
||||||
private var fBounds: IntRect = IntRect.Zero
|
private var mBounds: IntRect = IntRect.Zero
|
||||||
override fun set(rect: IntRect) { fBounds = rect }
|
override fun set(rect: IntRect) { mBounds = rect }
|
||||||
override fun intersect(region: SemanticsRegion): Boolean = false
|
override fun intersect(region: SemanticsRegion): Boolean = false
|
||||||
override fun difference(rect: IntRect): Boolean = false
|
override fun difference(rect: IntRect): Boolean = false
|
||||||
override val bounds: IntRect get() = fBounds
|
override val bounds: IntRect get() = mBounds
|
||||||
override val isEmpty: Boolean get() = fBounds == IntRect.Zero
|
override val isEmpty: Boolean get() = mBounds == IntRect.Zero
|
||||||
}
|
}
|
||||||
|
|
||||||
internal actual fun SemanticsRegion(): SemanticsRegion = StubSemanticsRegion()
|
internal actual fun SemanticsRegion(): SemanticsRegion = StubSemanticsRegion()
|
||||||
|
|||||||
+24
-14
@@ -5,19 +5,27 @@ import androidx.compose.ui.input.key.KeyEvent
|
|||||||
import androidx.compose.ui.input.key.KeyEventType
|
import androidx.compose.ui.input.key.KeyEventType
|
||||||
import androidx.compose.ui.input.pointer.PointerButton
|
import androidx.compose.ui.input.pointer.PointerButton
|
||||||
import androidx.compose.ui.input.pointer.PointerEventType
|
import androidx.compose.ui.input.pointer.PointerEventType
|
||||||
import com.compose.sdl.input.LegacyPointerEvent
|
|
||||||
import kotlinx.cinterop.*
|
import kotlinx.cinterop.*
|
||||||
import sdl3.*
|
import sdl3.*
|
||||||
|
|
||||||
// ==================
|
// ==================
|
||||||
// MARK: SDL3EventMapper
|
// MARK: AppEvent + the SDL3 event mapper
|
||||||
// ==================
|
// ==================
|
||||||
|
|
||||||
/** Events carry the SDL window id they belong to (0 = unknown - injected test
|
/** Events carry the SDL window id they belong to (0 = unknown - injected test
|
||||||
events; the loop routes those to its first window). Quit is app-level. */
|
events; the loop routes those to its first window). Quit is app-level. */
|
||||||
sealed class AppEvent {
|
sealed class AppEvent {
|
||||||
data object Quit : AppEvent()
|
data object Quit : AppEvent()
|
||||||
data class Pointer(val event: LegacyPointerEvent, val windowId: UInt = 0u) : AppEvent()
|
/** A single-pointer SDL mouse event. Flat like its MouseWheel sibling: the
|
||||||
|
fields used to sit behind a `LegacyPointerEvent` wrapper that the mapper
|
||||||
|
built and the window loop immediately took apart again. */
|
||||||
|
data class Pointer(
|
||||||
|
val x: Float,
|
||||||
|
val y: Float,
|
||||||
|
val type: PointerEventType,
|
||||||
|
val button: PointerButton = PointerButton.Primary,
|
||||||
|
val windowId: UInt = 0u,
|
||||||
|
) : AppEvent()
|
||||||
data class Key(val event: KeyEvent, val windowId: UInt = 0u) : AppEvent()
|
data class Key(val event: KeyEvent, val windowId: UInt = 0u) : AppEvent()
|
||||||
data class TextInput(val text: String, val windowId: UInt = 0u) : AppEvent()
|
data class TextInput(val text: String, val windowId: UInt = 0u) : AppEvent()
|
||||||
/** IME preedit / composition (SDL_EVENT_TEXT_EDITING). Empty text = composition
|
/** IME preedit / composition (SDL_EVENT_TEXT_EDITING). Empty text = composition
|
||||||
@@ -75,34 +83,36 @@ fun pollEvents(): List<AppEvent> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun mapEvent(e: SDL_Event): AppEvent? {
|
private fun mapEvent(e: SDL_Event): AppEvent? {
|
||||||
val type = e.type
|
return when (e.type) {
|
||||||
return when (type) {
|
|
||||||
SDL_EVENT_QUIT -> AppEvent.Quit
|
SDL_EVENT_QUIT -> AppEvent.Quit
|
||||||
|
|
||||||
SDL_EVENT_MOUSE_BUTTON_DOWN -> {
|
SDL_EVENT_MOUSE_BUTTON_DOWN -> {
|
||||||
val mb = e.button
|
val mb = e.button
|
||||||
AppEvent.Pointer(LegacyPointerEvent(
|
AppEvent.Pointer(
|
||||||
x = mb.x, y = mb.y,
|
x = mb.x, y = mb.y,
|
||||||
type = PointerEventType.Press,
|
type = PointerEventType.Press,
|
||||||
button = mapButton(mb.button)
|
button = mapButton(mb.button),
|
||||||
), mb.windowID)
|
windowId = mb.windowID,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_EVENT_MOUSE_BUTTON_UP -> {
|
SDL_EVENT_MOUSE_BUTTON_UP -> {
|
||||||
val mb = e.button
|
val mb = e.button
|
||||||
AppEvent.Pointer(LegacyPointerEvent(
|
AppEvent.Pointer(
|
||||||
x = mb.x, y = mb.y,
|
x = mb.x, y = mb.y,
|
||||||
type = PointerEventType.Release,
|
type = PointerEventType.Release,
|
||||||
button = mapButton(mb.button)
|
button = mapButton(mb.button),
|
||||||
), mb.windowID)
|
windowId = mb.windowID,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_EVENT_MOUSE_MOTION -> {
|
SDL_EVENT_MOUSE_MOTION -> {
|
||||||
val mm = e.motion
|
val mm = e.motion
|
||||||
AppEvent.Pointer(LegacyPointerEvent(
|
AppEvent.Pointer(
|
||||||
x = mm.x, y = mm.y,
|
x = mm.x, y = mm.y,
|
||||||
type = PointerEventType.Move
|
type = PointerEventType.Move,
|
||||||
), mm.windowID)
|
windowId = mm.windowID,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_EVENT_KEY_DOWN -> mapKey(e.key, KeyEventType.KeyDown)
|
SDL_EVENT_KEY_DOWN -> mapKey(e.key, KeyEventType.KeyDown)
|
||||||
@@ -5,14 +5,18 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.runtime.staticCompositionLocalOf
|
import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
import androidx.compose.ui.input.key.KeyEvent
|
import androidx.compose.ui.input.key.KeyEvent
|
||||||
|
import kotlinx.cinterop.COpaquePointer
|
||||||
import kotlinx.cinterop.reinterpret
|
import kotlinx.cinterop.reinterpret
|
||||||
import kotlinx.cinterop.toKString
|
|
||||||
import sdl3.SDL_GetRendererName
|
|
||||||
import sdl3.SDL_MaximizeWindow
|
import sdl3.SDL_MaximizeWindow
|
||||||
import sdl3.SDL_MinimizeWindow
|
import sdl3.SDL_MinimizeWindow
|
||||||
import sdl3.SDL_RaiseWindow
|
import sdl3.SDL_RaiseWindow
|
||||||
import sdl3.SDL_RestoreWindow
|
import sdl3.SDL_RestoreWindow
|
||||||
|
import sdl3.SDL_SetWindowAlwaysOnTop
|
||||||
|
import sdl3.SDL_SetWindowBordered
|
||||||
|
import sdl3.SDL_SetWindowFocusable
|
||||||
import sdl3.SDL_SetWindowFullscreen
|
import sdl3.SDL_SetWindowFullscreen
|
||||||
|
import sdl3.SDL_SetWindowOpacity
|
||||||
|
import sdl3.SDL_SetWindowResizable
|
||||||
import sdl3.SDL_SetWindowSize
|
import sdl3.SDL_SetWindowSize
|
||||||
import sdl3.SDL_SetWindowTitle
|
import sdl3.SDL_SetWindowTitle
|
||||||
|
|
||||||
@@ -30,46 +34,46 @@ import sdl3.SDL_SetWindowTitle
|
|||||||
them subscribes the caller to recomposition. The setter methods
|
them subscribes the caller to recomposition. The setter methods
|
||||||
call into SDL and also push the new value into the snapshot. */
|
call into SDL and also push the new value into the snapshot. */
|
||||||
class ComposeNativeWindow constructor(
|
class ComposeNativeWindow constructor(
|
||||||
private val backend: SDL3Backend,
|
private val backend: Sdl3Backend,
|
||||||
val gpuMode: GpuMode,
|
val gpuMode: GpuMode,
|
||||||
initialTitle: String,
|
initialTitle: String,
|
||||||
) {
|
) {
|
||||||
private var fWidth by mutableStateOf(backend.windowWidth)
|
private var mWidth by mutableStateOf(backend.windowWidth)
|
||||||
private var fHeight by mutableStateOf(backend.windowHeight)
|
private var mHeight by mutableStateOf(backend.windowHeight)
|
||||||
private var fPixelWidth by mutableStateOf(backend.pixelWidth)
|
private var mPixelWidth by mutableStateOf(backend.pixelWidth)
|
||||||
private var fPixelHeight by mutableStateOf(backend.pixelHeight)
|
private var mPixelHeight by mutableStateOf(backend.pixelHeight)
|
||||||
private var fTitle by mutableStateOf(initialTitle)
|
private var mTitle by mutableStateOf(initialTitle)
|
||||||
private var fMinimized by mutableStateOf(false)
|
private var mMinimized by mutableStateOf(false)
|
||||||
private var fMaximized by mutableStateOf(false)
|
private var mMaximized by mutableStateOf(false)
|
||||||
private var fFullscreen by mutableStateOf(false)
|
private var mFullscreen by mutableStateOf(false)
|
||||||
private var fCloseRequested by mutableStateOf(false)
|
private var mCloseRequested by mutableStateOf(false)
|
||||||
private var fFps by mutableStateOf(0)
|
private var mFps by mutableStateOf(0)
|
||||||
|
|
||||||
// ============
|
// ============
|
||||||
// State
|
// State
|
||||||
|
|
||||||
/** Logical (point) width - the same units layout uses. */
|
/** Logical (point) width - the same units layout uses. */
|
||||||
val width: Int get() = fWidth
|
val width: Int get() = mWidth
|
||||||
val height: Int get() = fHeight
|
val height: Int get() = mHeight
|
||||||
/** Physical (pixel) back-buffer size; on Retina this is `width * pixelDensity`. */
|
/** Physical (pixel) back-buffer size; on Retina this is `width * pixelDensity`. */
|
||||||
val pixelWidth: Int get() = fPixelWidth
|
val pixelWidth: Int get() = mPixelWidth
|
||||||
val pixelHeight: Int get() = fPixelHeight
|
val pixelHeight: Int get() = mPixelHeight
|
||||||
val title: String get() = fTitle
|
val title: String get() = mTitle
|
||||||
val isMinimized: Boolean get() = fMinimized
|
val isMinimized: Boolean get() = mMinimized
|
||||||
val isMaximized: Boolean get() = fMaximized
|
val isMaximized: Boolean get() = mMaximized
|
||||||
val isFullscreen: Boolean get() = fFullscreen
|
val isFullscreen: Boolean get() = mFullscreen
|
||||||
val pixelDensity: Float get() = backend.pixelDensity
|
val pixelDensity: Float get() = backend.pixelDensity
|
||||||
/** Frames per second, refreshed ~once a second by the render loop. Snapshot-
|
/** Frames per second, refreshed ~once a second by the render loop. Snapshot-
|
||||||
backed, so reading it in a composable recomposes when it changes. */
|
backed, so reading it in a composable recomposes when it changes. */
|
||||||
val fps: Int get() = fFps
|
val fps: Int get() = mFps
|
||||||
|
|
||||||
/** Human-readable name of the rendering pipeline:
|
/** Human-readable name of the rendering pipeline:
|
||||||
"Skia / Metal", "Skia / OpenGL", "Skia / CPU raster". */
|
"Skia / Metal", "Skia / OpenGL", "Skia / CPU raster". */
|
||||||
val rendererName: String
|
val rendererName: String
|
||||||
get() = when (gpuMode) {
|
get() = when (gpuMode) {
|
||||||
is GpuMode.Skia.Metal -> "Skia / Metal"
|
is GpuMode.Metal -> "Skia / Metal"
|
||||||
is GpuMode.Skia.OpenGL -> "Skia / OpenGL"
|
is GpuMode.OpenGL -> "Skia / OpenGL"
|
||||||
is GpuMode.Software -> "Skia / CPU raster"
|
is GpuMode.CpuRaster -> "Skia / CPU raster"
|
||||||
is GpuMode.Auto -> "Auto (unresolved)"
|
is GpuMode.Auto -> "Auto (unresolved)"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +83,7 @@ class ComposeNativeWindow constructor(
|
|||||||
fun setTitle(inTitle: String) {
|
fun setTitle(inTitle: String) {
|
||||||
val vWindow = backend.window?.reinterpret<cnames.structs.SDL_Window>() ?: return
|
val vWindow = backend.window?.reinterpret<cnames.structs.SDL_Window>() ?: return
|
||||||
SDL_SetWindowTitle(vWindow, inTitle)
|
SDL_SetWindowTitle(vWindow, inTitle)
|
||||||
fTitle = inTitle
|
mTitle = inTitle
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Logical size in points - SDL fires AppEvent.WindowResized which
|
/** Logical size in points - SDL fires AppEvent.WindowResized which
|
||||||
@@ -91,56 +95,111 @@ class ComposeNativeWindow constructor(
|
|||||||
|
|
||||||
fun minimize() {
|
fun minimize() {
|
||||||
val vWindow = backend.window?.reinterpret<cnames.structs.SDL_Window>() ?: return
|
val vWindow = backend.window?.reinterpret<cnames.structs.SDL_Window>() ?: return
|
||||||
SDL_MinimizeWindow(vWindow); fMinimized = true
|
SDL_MinimizeWindow(vWindow); mMinimized = true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun maximize() {
|
fun maximize() {
|
||||||
val vWindow = backend.window?.reinterpret<cnames.structs.SDL_Window>() ?: return
|
val vWindow = backend.window?.reinterpret<cnames.structs.SDL_Window>() ?: return
|
||||||
SDL_MaximizeWindow(vWindow); fMaximized = true; fMinimized = false
|
SDL_MaximizeWindow(vWindow); mMaximized = true; mMinimized = false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun restore() {
|
fun restore() {
|
||||||
val vWindow = backend.window?.reinterpret<cnames.structs.SDL_Window>() ?: return
|
val vWindow = backend.window?.reinterpret<cnames.structs.SDL_Window>() ?: return
|
||||||
SDL_RestoreWindow(vWindow); fMaximized = false; fMinimized = false
|
SDL_RestoreWindow(vWindow); mMaximized = false; mMinimized = false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setFullscreen(inFullscreen: Boolean) {
|
fun setFullscreen(inFullscreen: Boolean) {
|
||||||
val vWindow = backend.window?.reinterpret<cnames.structs.SDL_Window>() ?: return
|
val vWindow = backend.window?.reinterpret<cnames.structs.SDL_Window>() ?: return
|
||||||
SDL_SetWindowFullscreen(vWindow, inFullscreen); fFullscreen = inFullscreen
|
SDL_SetWindowFullscreen(vWindow, inFullscreen); mFullscreen = inFullscreen
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toggleFullscreen() = setFullscreen(!fFullscreen)
|
fun toggleFullscreen() = setFullscreen(!mFullscreen)
|
||||||
|
|
||||||
fun raise() {
|
fun raise() {
|
||||||
val vWindow = backend.window?.reinterpret<cnames.structs.SDL_Window>() ?: return
|
val vWindow = backend.window?.reinterpret<cnames.structs.SDL_Window>() ?: return
|
||||||
SDL_RaiseWindow(vWindow)
|
SDL_RaiseWindow(vWindow)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============
|
||||||
|
// Mutable window attributes (Compose Desktop's Window() parameters)
|
||||||
|
//
|
||||||
|
// Window() re-applies these from the composition whenever they change, so
|
||||||
|
// app code normally passes them as parameters rather than calling these.
|
||||||
|
// `transparent` is absent on purpose: SDL needs SDL_WINDOW_TRANSPARENT at
|
||||||
|
// creation and offers no setter, so it is a construction-time attribute.
|
||||||
|
|
||||||
|
private var mUndecorated = false
|
||||||
|
private var mResizable = true
|
||||||
|
private var mAlwaysOnTop = false
|
||||||
|
private var mFocusable = true
|
||||||
|
private var mEnabled = true
|
||||||
|
|
||||||
|
val isUndecorated: Boolean get() = mUndecorated
|
||||||
|
val isResizable: Boolean get() = mResizable
|
||||||
|
val isAlwaysOnTop: Boolean get() = mAlwaysOnTop
|
||||||
|
val isFocusable: Boolean get() = mFocusable
|
||||||
|
|
||||||
|
/** False hides the window's title bar and border. */
|
||||||
|
fun setUndecorated(inUndecorated: Boolean) {
|
||||||
|
val vWindow = backend.window?.reinterpret<cnames.structs.SDL_Window>() ?: return
|
||||||
|
SDL_SetWindowBordered(vWindow, !inUndecorated); mUndecorated = inUndecorated
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setResizable(inResizable: Boolean) {
|
||||||
|
val vWindow = backend.window?.reinterpret<cnames.structs.SDL_Window>() ?: return
|
||||||
|
SDL_SetWindowResizable(vWindow, inResizable); mResizable = inResizable
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setAlwaysOnTop(inAlwaysOnTop: Boolean) {
|
||||||
|
val vWindow = backend.window?.reinterpret<cnames.structs.SDL_Window>() ?: return
|
||||||
|
SDL_SetWindowAlwaysOnTop(vWindow, inAlwaysOnTop); mAlwaysOnTop = inAlwaysOnTop
|
||||||
|
}
|
||||||
|
|
||||||
|
/** False stops the window taking keyboard focus when clicked or raised. */
|
||||||
|
fun setFocusable(inFocusable: Boolean) {
|
||||||
|
val vWindow = backend.window?.reinterpret<cnames.structs.SDL_Window>() ?: return
|
||||||
|
SDL_SetWindowFocusable(vWindow, inFocusable); mFocusable = inFocusable
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Window opacity, 0f (fully transparent) to 1f. Needs a compositing window
|
||||||
|
manager; SDL reports failure on platforms without one. */
|
||||||
|
fun setOpacity(inOpacity: Float) {
|
||||||
|
val vWindow = backend.window?.reinterpret<cnames.structs.SDL_Window>() ?: return
|
||||||
|
SDL_SetWindowOpacity(vWindow, inOpacity.coerceIn(0f, 1f))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** True while the window accepts input. A disabled window still renders and
|
||||||
|
repaints; it just drops pointer / keyboard / text / wheel / drop events,
|
||||||
|
which is how Compose Desktop's `enabled = false` behaves. */
|
||||||
|
val isEnabled: Boolean get() = mEnabled
|
||||||
|
|
||||||
|
fun setEnabled(inEnabled: Boolean) { mEnabled = inEnabled }
|
||||||
|
|
||||||
/** Asks composeWindow's main loop to break out at the next frame.
|
/** Asks composeWindow's main loop to break out at the next frame.
|
||||||
Same effect as the user closing the window via the OS. Bypasses any
|
Same effect as the user closing the window via the OS. Bypasses any
|
||||||
onCloseRequest handler - this is the "really quit now" path. */
|
onCloseRequest handler - this is the "really quit now" path. */
|
||||||
fun close() { fCloseRequested = true }
|
fun close() { mCloseRequested = true }
|
||||||
|
|
||||||
// ============
|
// ============
|
||||||
// Close interception
|
// Close interception
|
||||||
|
|
||||||
private var fOnCloseRequest: (() -> Boolean)? = null
|
private var mOnCloseRequest: (() -> Boolean)? = null
|
||||||
|
|
||||||
/** Register a handler invoked when the user tries to close the window (OS
|
/** Register a handler invoked when the user tries to close the window (OS
|
||||||
close button / Quit). Return true to let the close proceed, false to veto
|
close button / Quit). Return true to let the close proceed, false to veto
|
||||||
it (e.g. to show an "unsaved changes" dialog first, then call close()
|
it (e.g. to show an "unsaved changes" dialog first, then call close()
|
||||||
once the user confirms). Pass null to clear. */
|
once the user confirms). Pass null to clear. */
|
||||||
fun setOnCloseRequest(inHandler: (() -> Boolean)?) { fOnCloseRequest = inHandler }
|
fun setOnCloseRequest(inHandler: (() -> Boolean)?) { mOnCloseRequest = inHandler }
|
||||||
|
|
||||||
// ============
|
// ============
|
||||||
// Global key shortcuts
|
// Global key shortcuts
|
||||||
|
|
||||||
private var fOnKeyShortcut: ((KeyEvent) -> Boolean)? = null
|
private var mOnKeyShortcut: ((KeyEvent) -> Boolean)? = null
|
||||||
|
|
||||||
/** Register a handler for key events the focused node didn't consume - for
|
/** Register a handler for key events the focused node didn't consume - for
|
||||||
app-wide shortcuts (Ctrl+S, etc.). Receives every unconsumed key (and all
|
app-wide shortcuts (Ctrl+S, etc.). Receives every unconsumed key (and all
|
||||||
keys when nothing is focused). Return true if handled. Pass null to clear. */
|
keys when nothing is focused). Return true if handled. Pass null to clear. */
|
||||||
fun setOnKeyShortcut(inHandler: ((KeyEvent) -> Boolean)?) { fOnKeyShortcut = inHandler }
|
fun setOnKeyShortcut(inHandler: ((KeyEvent) -> Boolean)?) { mOnKeyShortcut = inHandler }
|
||||||
|
|
||||||
// ============
|
// ============
|
||||||
// Framework hooks - driven by composeWindow's main loop (which lives in
|
// Framework hooks - driven by composeWindow's main loop (which lives in
|
||||||
@@ -148,26 +207,71 @@ class ComposeNativeWindow constructor(
|
|||||||
// intended for app code.
|
// intended for app code.
|
||||||
|
|
||||||
fun onResized() {
|
fun onResized() {
|
||||||
fWidth = backend.windowWidth
|
mWidth = backend.windowWidth
|
||||||
fHeight = backend.windowHeight
|
mHeight = backend.windowHeight
|
||||||
fPixelWidth = backend.pixelWidth
|
mPixelWidth = backend.pixelWidth
|
||||||
fPixelHeight = backend.pixelHeight
|
mPixelHeight = backend.pixelHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Called by the main loop ~once a second with the measured frame rate. */
|
/** Called by the main loop ~once a second with the measured frame rate. */
|
||||||
fun updateFps(inFps: Int) { fFps = inFps }
|
fun updateFps(inFps: Int) { mFps = inFps }
|
||||||
|
|
||||||
val isCloseRequested: Boolean get() = fCloseRequested
|
val isCloseRequested: Boolean get() = mCloseRequested
|
||||||
|
|
||||||
/** Driven by composeWindow's main loop on an OS close / Quit event. Returns
|
/** Driven by composeWindow's main loop on an OS close / Quit event. Returns
|
||||||
true if the close should proceed (no handler, or the handler allowed it). */
|
true if the close should proceed (no handler, or the handler allowed it). */
|
||||||
fun requestCloseFromUser(): Boolean = fOnCloseRequest?.invoke() ?: true
|
fun requestCloseFromUser(): Boolean = mOnCloseRequest?.invoke() ?: true
|
||||||
|
|
||||||
/** Driven by composeWindow's main loop for keys the focused node didn't
|
/** Driven by composeWindow's main loop for keys the focused node didn't
|
||||||
consume. Returns true if the shortcut handler handled it. */
|
consume. Returns true if the shortcut handler handled it. */
|
||||||
fun dispatchKeyShortcut(inEvent: KeyEvent): Boolean = fOnKeyShortcut?.invoke(inEvent) ?: false
|
fun dispatchKeyShortcut(inEvent: KeyEvent): Boolean = mOnKeyShortcut?.invoke(inEvent) ?: false
|
||||||
|
|
||||||
|
// ============
|
||||||
|
// Raw SDL escape hatch
|
||||||
|
|
||||||
|
/**
|
||||||
|
The live SDL handles behind this window, for calling the `sdl3.*` cinterop
|
||||||
|
directly when the Compose-level API doesn't cover something.
|
||||||
|
|
||||||
|
Returns a snapshot: the pointers are only valid while the window is alive,
|
||||||
|
so re-read it rather than caching, and never use it after the window closes.
|
||||||
|
Whatever you do through it is outside the port's control - resizing,
|
||||||
|
destroying or re-creating the window or its renderer behind Compose's back
|
||||||
|
will desync the renderer state.
|
||||||
|
*/
|
||||||
|
fun rawSdlHandles(): RawSdlHandles = RawSdlHandles(
|
||||||
|
window = backend.window,
|
||||||
|
renderer = backend.renderer,
|
||||||
|
glContext = backend.glContext,
|
||||||
|
metalView = backend.metalView,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Opaque SDL pointers for one window, as handed out by
|
||||||
|
[ComposeNativeWindow.rawSdlHandles].
|
||||||
|
|
||||||
|
`reinterpret()` each to the `cnames.structs.*` type the SDL function expects:
|
||||||
|
|
||||||
|
val vHandles = window.rawSdlHandles()
|
||||||
|
vHandles.window?.let { sdl3.SDL_FlashWindow(it.reinterpret(), FLASH_BRIEFLY) }
|
||||||
|
|
||||||
|
Which pointers are non-null depends on the resolved [GpuMode]: `renderer` is
|
||||||
|
set only for the CPU-raster (Software) backend, `glContext` only for Skia
|
||||||
|
OpenGL, and `metalView` only for Skia Metal. `window` is always set on a
|
||||||
|
successfully created window.
|
||||||
|
*/
|
||||||
|
data class RawSdlHandles(
|
||||||
|
/** `SDL_Window*` - always present while the window lives. */
|
||||||
|
val window: COpaquePointer?,
|
||||||
|
/** `SDL_Renderer*` - CPU-raster (Software) backend only. */
|
||||||
|
val renderer: COpaquePointer?,
|
||||||
|
/** `SDL_GLContext` - Skia OpenGL backend only. */
|
||||||
|
val glContext: COpaquePointer?,
|
||||||
|
/** `SDL_MetalView` - Skia Metal backend only. */
|
||||||
|
val metalView: COpaquePointer?,
|
||||||
|
)
|
||||||
|
|
||||||
// ==================
|
// ==================
|
||||||
// MARK: Scope + CompositionLocal
|
// MARK: Scope + CompositionLocal
|
||||||
// ==================
|
// ==================
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import sdl3.SDL_ShowSaveFileDialog
|
|||||||
|
|
||||||
The C callback can't capture Kotlin state, so the result handler is parked in
|
The C callback can't capture Kotlin state, so the result handler is parked in
|
||||||
a StableRef passed as the dialog's `userdata` and disposed inside the callback. */
|
a StableRef passed as the dialog's `userdata` and disposed inside the callback. */
|
||||||
private val fDialogCallback = staticCFunction { inUserData: COpaquePointer?, inFileList: CPointer<CPointerVar<ByteVar>>?, inFilter: Int ->
|
private val mDialogCallback = staticCFunction { inUserData: COpaquePointer?, inFileList: CPointer<CPointerVar<ByteVar>>?, inFilter: Int ->
|
||||||
inFilter.hashCode() // unused (selected filter index)
|
inFilter.hashCode() // unused (selected filter index)
|
||||||
val vRef = inUserData?.asStableRef<(String?) -> Unit>() ?: return@staticCFunction
|
val vRef = inUserData?.asStableRef<(String?) -> Unit>() ?: return@staticCFunction
|
||||||
val vHandler = vRef.get()
|
val vHandler = vRef.get()
|
||||||
@@ -31,7 +31,7 @@ private val fDialogCallback = staticCFunction { inUserData: COpaquePointer?, inF
|
|||||||
fun showSaveFileDialog(inDefaultName: String? = null, inOnResult: (String?) -> Unit) {
|
fun showSaveFileDialog(inDefaultName: String? = null, inOnResult: (String?) -> Unit) {
|
||||||
val vRef = StableRef.create(inOnResult)
|
val vRef = StableRef.create(inOnResult)
|
||||||
SDL_ShowSaveFileDialog(
|
SDL_ShowSaveFileDialog(
|
||||||
fDialogCallback,
|
mDialogCallback,
|
||||||
vRef.asCPointer(),
|
vRef.asCPointer(),
|
||||||
null, // parent window (null = unparented)
|
null, // parent window (null = unparented)
|
||||||
null, // no file-type filters
|
null, // no file-type filters
|
||||||
@@ -45,7 +45,7 @@ fun showSaveFileDialog(inDefaultName: String? = null, inOnResult: (String?) -> U
|
|||||||
fun showOpenFileDialog(inOnResult: (String?) -> Unit) {
|
fun showOpenFileDialog(inOnResult: (String?) -> Unit) {
|
||||||
val vRef = StableRef.create(inOnResult)
|
val vRef = StableRef.create(inOnResult)
|
||||||
SDL_ShowOpenFileDialog(
|
SDL_ShowOpenFileDialog(
|
||||||
fDialogCallback,
|
mDialogCallback,
|
||||||
vRef.asCPointer(),
|
vRef.asCPointer(),
|
||||||
null, // parent window
|
null, // parent window
|
||||||
null, // filters
|
null, // filters
|
||||||
|
|||||||
@@ -4,28 +4,38 @@ package com.compose.sdl
|
|||||||
// MARK: GpuMode
|
// MARK: GpuMode
|
||||||
// ==================
|
// ==================
|
||||||
|
|
||||||
/** Which GPU path the Skia renderer uses.
|
/**
|
||||||
*
|
Which drawing path the renderer uses.
|
||||||
* ┌─ Auto - pick the best for this platform (Metal on macOS, OpenGL on
|
|
||||||
* │ Linux, OpenGL on Windows). If the GPU context/bridge can't be
|
Every mode is Skia - the port renders through Skia on all targets. The axis
|
||||||
* │ created, ComposeWindow falls back to Software.
|
here is only how Skia's output reaches the screen:
|
||||||
* ├─ Software - Skia CPU raster: paints a host pixel buffer that SDL_Renderer
|
|
||||||
* │ uploads as a texture each frame. No GPU context needed.
|
Auto resolve per platform at window creation (Metal on macOS, OpenGL
|
||||||
* └─ Skia.* - Skia GPU bridges
|
on Linux and Windows). If the GPU context can't be created, the
|
||||||
* ├─ OpenGL - Skia GL backend on an SDL OpenGL (WGL/GLX) context
|
window falls back to CpuRaster rather than failing to open.
|
||||||
* └─ Metal - Skia Metal on a CAMetalLayer via SDL_Metal_CreateView (macOS)
|
Metal Skia's Metal backend on a CAMetalLayer (SDL_Metal_CreateView), macOS.
|
||||||
*
|
OpenGL Skia's GL backend on an SDL OpenGL (WGL/GLX) context.
|
||||||
* AUTO resolves via rendererPreferredGpuMode() per target, at composeWindow entry. */
|
CpuRaster Skia paints a host pixel buffer that SDL_Renderer uploads as a
|
||||||
|
texture each frame. No GPU context needed.
|
||||||
|
|
||||||
|
This used to read `Software` vs a nested `Skia.*` group, which dated from when
|
||||||
|
the alternative to Skia was SDL's own renderer. With SDL reduced to windowing
|
||||||
|
that split was actively misleading - "Software" was always Skia too.
|
||||||
|
*/
|
||||||
sealed class GpuMode {
|
sealed class GpuMode {
|
||||||
/** Let the platform pick: Skia.Metal on macOS, Skia.OpenGL on Linux/Windows. */
|
/** Let the platform pick: [Metal] on macOS, [OpenGL] on Linux and Windows. */
|
||||||
object Auto : GpuMode() { override fun toString() = "Auto" }
|
object Auto : GpuMode() { override fun toString() = "Auto" }
|
||||||
|
|
||||||
/** Skia CPU raster - a host pixel buffer uploaded via SDL_Renderer each frame. */
|
/** Skia Metal on a CAMetalLayer via SDL_Metal_CreateView (macOS only). */
|
||||||
object Software : GpuMode() { override fun toString() = "Software" }
|
object Metal : GpuMode() { override fun toString() = "Metal" }
|
||||||
|
|
||||||
/** Skia GPU bridges. */
|
/** Skia GL on an SDL OpenGL context (WGL on Windows, GLX on Linux). */
|
||||||
sealed class Skia : GpuMode() {
|
object OpenGL : GpuMode() { override fun toString() = "OpenGL" }
|
||||||
object OpenGL : Skia() { override fun toString() = "Skia.OpenGL" }
|
|
||||||
object Metal : Skia() { override fun toString() = "Skia.Metal" }
|
/** Skia CPU raster - a host pixel buffer uploaded via SDL_Renderer each frame. */
|
||||||
}
|
object CpuRaster : GpuMode() { override fun toString() = "CpuRaster" }
|
||||||
|
|
||||||
|
/** True for the GPU-backed modes. [Auto] is false because it is unresolved:
|
||||||
|
ask after the window has resolved it, via ComposeNativeWindow.gpuMode. */
|
||||||
|
val isGpuAccelerated: Boolean get() = this is Metal || this is OpenGL
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import sdl3.SDL_OpenURL
|
|||||||
UI until the file manager was up. None of the three platform backends
|
UI until the file manager was up. None of the three platform backends
|
||||||
requires the main thread, so a small background scope launches them and
|
requires the main thread, so a small background scope launches them and
|
||||||
(optionally) posts the result back to Main. */
|
(optionally) posts the result back to Main. */
|
||||||
private val fOpenExternalScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
private val mOpenExternalScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||||
|
|
||||||
/** Opens a URL or file:// URI with the OS's default handler (browser, file
|
/** Opens a URL or file:// URI with the OS's default handler (browser, file
|
||||||
manager, …) via SDL_OpenURL - asynchronously, so the UI keeps pumping while
|
manager, …) via SDL_OpenURL - asynchronously, so the UI keeps pumping while
|
||||||
@@ -30,7 +30,7 @@ private val fOpenExternalScope = CoroutineScope(SupervisorJob() + Dispatchers.De
|
|||||||
thread with SDL_OpenURL's success flag. */
|
thread with SDL_OpenURL's success flag. */
|
||||||
@OptIn(ExperimentalForeignApi::class)
|
@OptIn(ExperimentalForeignApi::class)
|
||||||
fun openUrl(inUrl: String, inOnResult: ((Boolean) -> Unit)? = null) {
|
fun openUrl(inUrl: String, inOnResult: ((Boolean) -> Unit)? = null) {
|
||||||
fOpenExternalScope.launch {
|
mOpenExternalScope.launch {
|
||||||
val vOk = SDL_OpenURL(inUrl)
|
val vOk = SDL_OpenURL(inUrl)
|
||||||
if (inOnResult != null) withContext(Dispatchers.Main) { inOnResult(vOk) }
|
if (inOnResult != null) withContext(Dispatchers.Main) { inOnResult(vOk) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,21 @@ import com.compose.sdl.res.ImageLoader
|
|||||||
(official Skiko on macOS/Linux, the fork on mingwX64); text measurement +
|
(official Skiko on macOS/Linux, the fork on mingwX64); text measurement +
|
||||||
drawing go through the skiko paragraph engine (androidx.compose.ui.text
|
drawing go through the skiko paragraph engine (androidx.compose.ui.text
|
||||||
Paragraph actuals), so there is no renderer-owned measurer here. */
|
Paragraph actuals), so there is no renderer-owned measurer here. */
|
||||||
|
/*
|
||||||
|
WHY THIS INTERFACE EXISTS WITH ONE IMPLEMENTATION - do not "simplify" it away.
|
||||||
|
|
||||||
|
SkiaRenderBackend is its only implementor, which reads like leftover
|
||||||
|
dual-backend scaffolding from when SDL's own renderer was the alternative. It
|
||||||
|
is not. SkiaRenderBackend lives in skikoRendererMain, BELOW nativeMain, so
|
||||||
|
nativeMain-level code cannot name it: :desktop-native-window's WindowInstance
|
||||||
|
holds the renderer in nativeMain and would not compile against the concrete
|
||||||
|
type. Verified by trying it - compileNativeMainKotlinMetadata fails with
|
||||||
|
"Unresolved reference 'renderer'".
|
||||||
|
|
||||||
|
So this interface is the nativeMain-visible boundary for an implementation
|
||||||
|
that must live lower down, exactly like the expect fun createRenderBackend
|
||||||
|
that returns it. Same reasoning, same constraint.
|
||||||
|
*/
|
||||||
interface RenderBackend {
|
interface RenderBackend {
|
||||||
/** Loader the shared ImageMeasurePolicy / Res.readBytes plug into. Backed
|
/** Loader the shared ImageMeasurePolicy / Res.readBytes plug into. Backed
|
||||||
by the same decode cache the renderer uses to paint images, so a
|
by the same decode cache the renderer uses to paint images, so a
|
||||||
@@ -30,8 +45,13 @@ interface RenderBackend {
|
|||||||
DrawModifierNode pipeline. Taking a (Canvas)->Unit instead of the host keeps
|
DrawModifierNode pipeline. Taking a (Canvas)->Unit instead of the host keeps
|
||||||
RenderBackend + its implementations independent of the node/host layer - the
|
RenderBackend + its implementations independent of the node/host layer - the
|
||||||
decoupling that lets the renderers live in :ui-graphics (no ui-graphics→ui
|
decoupling that lets the renderers live in :ui-graphics (no ui-graphics→ui
|
||||||
cycle). Default no-op. */
|
cycle).
|
||||||
fun drawRoot(inDraw: (canvas: androidx.compose.ui.graphics.Canvas) -> Unit) {}
|
|
||||||
|
ABSTRACT, not a no-op default: the empty body dates from when a second,
|
||||||
|
non-Skia backend could opt out of it, and with the Skia backend now the
|
||||||
|
only implementation an unimplemented drawRoot would silently render
|
||||||
|
nothing rather than fail to compile. */
|
||||||
|
fun drawRoot(inDraw: (canvas: androidx.compose.ui.graphics.Canvas) -> Unit)
|
||||||
|
|
||||||
/** Flush + present whatever was just drawn. */
|
/** Flush + present whatever was just drawn. */
|
||||||
fun endFrame()
|
fun endFrame()
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ package com.compose.sdl
|
|||||||
|
|
||||||
/** Create the render backend for the selected [GpuMode]; null when the
|
/** Create the render backend for the selected [GpuMode]; null when the
|
||||||
backend can't initialise (caller falls back / reports). */
|
backend can't initialise (caller falls back / reports). */
|
||||||
expect fun createRenderBackend(inSdl: SDL3Backend, inGpu: GpuMode): RenderBackend?
|
expect fun createRenderBackend(inSdl: Sdl3Backend, inGpu: GpuMode): RenderBackend?
|
||||||
|
|
||||||
/** The renderer module's per-OS default GpuMode (used for GpuMode.Auto). */
|
/** The renderer module's per-OS default GpuMode (used for GpuMode.Auto). */
|
||||||
expect fun rendererPreferredGpuMode(): GpuMode
|
expect fun rendererPreferredGpuMode(): GpuMode
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ import sdl3.SDL_GetBasePath
|
|||||||
// no zlib/gzip wrapper). ZIP64 is not supported - the resource set is
|
// no zlib/gzip wrapper). ZIP64 is not supported - the resource set is
|
||||||
// small enough that the standard 4 GB / 65535-entry limits don't apply.
|
// small enough that the standard 4 GB / 65535-entry limits don't apply.
|
||||||
|
|
||||||
private class ComposeResourceArchive(private val fHandle: FileHandle) {
|
private class ComposeResourceArchive(private val mHandle: FileHandle) {
|
||||||
|
|
||||||
// One entry's location + sizing + compression method from the central
|
// One entry's location + sizing + compression method from the central
|
||||||
// directory. method 0 = stored, 8 = deflated.
|
// directory. method 0 = stored, 8 = deflated.
|
||||||
@@ -53,18 +53,18 @@ private class ComposeResourceArchive(private val fHandle: FileHandle) {
|
|||||||
val uncompressedSize: Long,
|
val uncompressedSize: Long,
|
||||||
val method: Int,
|
val method: Int,
|
||||||
)
|
)
|
||||||
private val fEntries: Map<String, Entry>
|
private val mEntries: Map<String, Entry>
|
||||||
|
|
||||||
init {
|
init {
|
||||||
fEntries = readCentralDirectory()
|
mEntries = readCentralDirectory()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun has(inPath: String): Boolean = fEntries.containsKey(inPath)
|
fun has(inPath: String): Boolean = mEntries.containsKey(inPath)
|
||||||
|
|
||||||
/** Reads an entry's bytes (inflated if needed) or null if the entry is
|
/** Reads an entry's bytes (inflated if needed) or null if the entry is
|
||||||
missing / uses an unsupported compression method. */
|
missing / uses an unsupported compression method. */
|
||||||
fun readBytes(inPath: String): ByteArray? {
|
fun readBytes(inPath: String): ByteArray? {
|
||||||
val vEntry = fEntries[inPath] ?: return null
|
val vEntry = mEntries[inPath] ?: return null
|
||||||
if (vEntry.uncompressedSize <= 0L) return ByteArray(0)
|
if (vEntry.uncompressedSize <= 0L) return ByteArray(0)
|
||||||
|
|
||||||
// Local file header layout: 30 fixed bytes, then filename_len + extra_len.
|
// Local file header layout: 30 fixed bytes, then filename_len + extra_len.
|
||||||
@@ -126,7 +126,7 @@ private class ComposeResourceArchive(private val fHandle: FileHandle) {
|
|||||||
// End of Central Directory record: 22 bytes minimum, with up to 65535
|
// End of Central Directory record: 22 bytes minimum, with up to 65535
|
||||||
// bytes of comment trailing. Read the last 64 KiB + 22 and scan backward
|
// bytes of comment trailing. Read the last 64 KiB + 22 and scan backward
|
||||||
// for the EOCD signature.
|
// for the EOCD signature.
|
||||||
val vFileLen: Long = fHandle.size()
|
val vFileLen: Long = mHandle.size()
|
||||||
if (vFileLen < 22L) return emptyMap()
|
if (vFileLen < 22L) return emptyMap()
|
||||||
val vTailLen = minOf(vFileLen, 65557L).toInt()
|
val vTailLen = minOf(vFileLen, 65557L).toInt()
|
||||||
val vTail = ByteArray(vTailLen)
|
val vTail = ByteArray(vTailLen)
|
||||||
@@ -182,14 +182,14 @@ private class ComposeResourceArchive(private val fHandle: FileHandle) {
|
|||||||
if (outBuf.isEmpty()) return true
|
if (outBuf.isEmpty()) return true
|
||||||
var vRead = 0
|
var vRead = 0
|
||||||
while (vRead < outBuf.size) {
|
while (vRead < outBuf.size) {
|
||||||
val vN = fHandle.read(inOffset + vRead, outBuf, vRead, outBuf.size - vRead)
|
val vN = mHandle.read(inOffset + vRead, outBuf, vRead, outBuf.size - vRead)
|
||||||
if (vN <= 0) return false
|
if (vN <= 0) return false
|
||||||
vRead += vN
|
vRead += vN
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun close() = fHandle.close()
|
fun close() = mHandle.close()
|
||||||
|
|
||||||
private fun le16(inBuf: ByteArray, inOff: Int): Int =
|
private fun le16(inBuf: ByteArray, inOff: Int): Int =
|
||||||
(inBuf[inOff].toInt() and 0xFF) or
|
(inBuf[inOff].toInt() and 0xFF) or
|
||||||
|
|||||||
+27
-12
@@ -4,23 +4,32 @@ import kotlinx.cinterop.*
|
|||||||
import sdl3.*
|
import sdl3.*
|
||||||
|
|
||||||
// ==================
|
// ==================
|
||||||
// MARK: SDL3Backend
|
// MARK: Sdl3Backend
|
||||||
// ==================
|
// ==================
|
||||||
|
|
||||||
class SDL3Backend(
|
class Sdl3Backend(
|
||||||
private val title: String = "ComposeNativeSDL3",
|
private val title: String = "ComposeNativeSDL3",
|
||||||
private val width: Int = 800,
|
private val width: Int = 800,
|
||||||
private val height: Int = 600,
|
private val height: Int = 600,
|
||||||
val gpuMode: GpuMode = GpuMode.Software,
|
val gpuMode: GpuMode = GpuMode.CpuRaster,
|
||||||
// Resource paths (inside data.kres) of the pre-decoded .rgba icon blobs.
|
// Resource paths (inside data.kres) of the pre-decoded .rgba icon blobs.
|
||||||
// The first-listed size is irrelevant - the largest becomes the base and
|
// The first-listed size is irrelevant - the largest becomes the base and
|
||||||
// the rest become alternate resolutions. Dark set falls back to light.
|
// the rest become alternate resolutions. Dark set falls back to light.
|
||||||
private val iconLightResourcePaths: List<String> = emptyList(),
|
private val iconLightResourcePaths: List<String> = emptyList(),
|
||||||
private val iconDarkResourcePaths: List<String> = emptyList(),
|
private val iconDarkResourcePaths: List<String> = emptyList(),
|
||||||
|
// Creation-time window attributes, mirroring Compose Desktop's Window().
|
||||||
|
// `undecorated`, `resizable`, `alwaysOnTop` and `focusable` can also be
|
||||||
|
// changed later (see ComposeNativeWindow); `transparent` cannot - SDL needs
|
||||||
|
// SDL_WINDOW_TRANSPARENT at creation, so it is honoured here only.
|
||||||
|
private val undecorated: Boolean = false,
|
||||||
|
private val transparent: Boolean = false,
|
||||||
|
private val resizable: Boolean = true,
|
||||||
|
private val alwaysOnTop: Boolean = false,
|
||||||
|
private val focusable: Boolean = true,
|
||||||
) {
|
) {
|
||||||
init {
|
init {
|
||||||
require(gpuMode !is GpuMode.Auto) {
|
require(gpuMode !is GpuMode.Auto) {
|
||||||
"SDL3Backend received GpuMode.Auto - resolve via preferredGpuMode() first"
|
"Sdl3Backend received GpuMode.Auto - resolve via preferredGpuMode() first"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +75,7 @@ class SDL3Backend(
|
|||||||
com.compose.sdl.res.preferredLocaleProvider = ::sdlPreferredLocale
|
com.compose.sdl.res.preferredLocaleProvider = ::sdlPreferredLocale
|
||||||
com.compose.sdl.res.preferredLocaleTagsProvider = { com.compose.sdl.text.systemPreferredLocaleTags() }
|
com.compose.sdl.res.preferredLocaleTagsProvider = { com.compose.sdl.text.systemPreferredLocaleTags() }
|
||||||
|
|
||||||
if (gpuMode is GpuMode.Skia.OpenGL) {
|
if (gpuMode is GpuMode.OpenGL) {
|
||||||
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_CONTEXT_MAJOR_VERSION, 3)
|
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_CONTEXT_MAJOR_VERSION, 3)
|
||||||
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_CONTEXT_MINOR_VERSION, 2)
|
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_CONTEXT_MINOR_VERSION, 2)
|
||||||
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_CONTEXT_PROFILE_MASK,
|
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_CONTEXT_PROFILE_MASK,
|
||||||
@@ -75,10 +84,16 @@ class SDL3Backend(
|
|||||||
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_STENCIL_SIZE, 8)
|
SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_STENCIL_SIZE, 8)
|
||||||
}
|
}
|
||||||
|
|
||||||
val flags = SDL_WINDOW_RESIZABLE or SDL_WINDOW_HIGH_PIXEL_DENSITY or when (gpuMode) {
|
var attrFlags = SDL_WINDOW_HIGH_PIXEL_DENSITY
|
||||||
is GpuMode.Skia.OpenGL -> SDL_WINDOW_OPENGL
|
if (resizable) attrFlags = attrFlags or SDL_WINDOW_RESIZABLE
|
||||||
is GpuMode.Skia.Metal -> SDL_WINDOW_METAL
|
if (undecorated) attrFlags = attrFlags or SDL_WINDOW_BORDERLESS
|
||||||
is GpuMode.Software -> 0UL
|
if (transparent) attrFlags = attrFlags or SDL_WINDOW_TRANSPARENT
|
||||||
|
if (alwaysOnTop) attrFlags = attrFlags or SDL_WINDOW_ALWAYS_ON_TOP
|
||||||
|
if (!focusable) attrFlags = attrFlags or SDL_WINDOW_NOT_FOCUSABLE
|
||||||
|
val flags = attrFlags or when (gpuMode) {
|
||||||
|
is GpuMode.OpenGL -> SDL_WINDOW_OPENGL
|
||||||
|
is GpuMode.Metal -> SDL_WINDOW_METAL
|
||||||
|
is GpuMode.CpuRaster -> 0UL
|
||||||
is GpuMode.Auto -> error("unreachable")
|
is GpuMode.Auto -> error("unreachable")
|
||||||
}
|
}
|
||||||
window = SDL_CreateWindow(title, width, height, flags)
|
window = SDL_CreateWindow(title, width, height, flags)
|
||||||
@@ -88,7 +103,7 @@ class SDL3Backend(
|
|||||||
}
|
}
|
||||||
|
|
||||||
when (gpuMode) {
|
when (gpuMode) {
|
||||||
is GpuMode.Skia.OpenGL -> {
|
is GpuMode.OpenGL -> {
|
||||||
glContext = SDL_GL_CreateContext(window?.reinterpret())
|
glContext = SDL_GL_CreateContext(window?.reinterpret())
|
||||||
if (glContext == null) {
|
if (glContext == null) {
|
||||||
println("SDL_GL_CreateContext failed: ${SDL_GetError()?.toKString()}")
|
println("SDL_GL_CreateContext failed: ${SDL_GetError()?.toKString()}")
|
||||||
@@ -101,7 +116,7 @@ class SDL3Backend(
|
|||||||
// keeps its fallback frame cap.
|
// keeps its fallback frame cap.
|
||||||
vsyncEnabled = SDL_GL_SetSwapInterval(1)
|
vsyncEnabled = SDL_GL_SetSwapInterval(1)
|
||||||
}
|
}
|
||||||
is GpuMode.Skia.Metal -> {
|
is GpuMode.Metal -> {
|
||||||
metalView = SDL_Metal_CreateView(window?.reinterpret())
|
metalView = SDL_Metal_CreateView(window?.reinterpret())
|
||||||
if (metalView == null) {
|
if (metalView == null) {
|
||||||
println("SDL_Metal_CreateView failed: ${SDL_GetError()?.toKString()}")
|
println("SDL_Metal_CreateView failed: ${SDL_GetError()?.toKString()}")
|
||||||
@@ -111,7 +126,7 @@ class SDL3Backend(
|
|||||||
// blocks - so Metal is vsync-paced and the loop skips its delay.
|
// blocks - so Metal is vsync-paced and the loop skips its delay.
|
||||||
vsyncEnabled = true
|
vsyncEnabled = true
|
||||||
}
|
}
|
||||||
is GpuMode.Software -> {
|
is GpuMode.CpuRaster -> {
|
||||||
// Skia CPU raster: SkiaSurfaceBridge paints a host buffer that
|
// Skia CPU raster: SkiaSurfaceBridge paints a host buffer that
|
||||||
// this SDL_Renderer uploads as a texture each frame.
|
// this SDL_Renderer uploads as a texture each frame.
|
||||||
renderer = SDL_CreateRenderer(window?.reinterpret(), null)
|
renderer = SDL_CreateRenderer(window?.reinterpret(), null)
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
package com.compose.sdl.input
|
|
||||||
|
|
||||||
import androidx.compose.ui.input.pointer.PointerButton
|
|
||||||
import androidx.compose.ui.input.pointer.PointerEventType
|
|
||||||
|
|
||||||
// ==================
|
|
||||||
// MARK: LegacyPointerEvent (project-only)
|
|
||||||
// ==================
|
|
||||||
|
|
||||||
/** Project-only legacy event class used by the SDL3 input pipeline
|
|
||||||
(`SDL3EventMapper` → `AppEvent.Pointer` → `ComposeWindow`'s event loop).
|
|
||||||
The name `PointerEvent` is now reserved for upstream's multi-touch
|
|
||||||
vendored event (in androidx.compose.ui.input.pointer.PointerEvent);
|
|
||||||
this single-pointer (x, y, type, button) shape is the bridge between
|
|
||||||
raw SDL events and upstream-shaped PointerInputChange dispatch.
|
|
||||||
|
|
||||||
ComposeWindow constructs an upstream PointerEvent from this for
|
|
||||||
delivery to Modifier.pointerInput blocks. */
|
|
||||||
class LegacyPointerEvent(
|
|
||||||
// Kept as Float: SDL delivers sub-pixel cursor coordinates, and on HiDPI the
|
|
||||||
// value is multiplied by DPR before dispatch, so truncating to Int here would
|
|
||||||
// quantize the caret to whole-DPR steps (2px on a 2x display) near glyph edges.
|
|
||||||
val x: Float,
|
|
||||||
val y: Float,
|
|
||||||
val type: PointerEventType,
|
|
||||||
val button: PointerButton = PointerButton.Primary,
|
|
||||||
) {
|
|
||||||
override fun equals(other: Any?): Boolean = other is LegacyPointerEvent &&
|
|
||||||
other.x == x && other.y == y && other.type == type && other.button == button
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
var h = x.hashCode(); h = 31 * h + y.hashCode(); h = 31 * h + type.hashCode(); h = 31 * h + button.hashCode(); return h
|
|
||||||
}
|
|
||||||
override fun toString(): String =
|
|
||||||
"LegacyPointerEvent(x=$x, y=$y, type=$type, button=$button)"
|
|
||||||
}
|
|
||||||
@@ -29,12 +29,12 @@ class ComposeRootHost(inDensity: Float = 1f) {
|
|||||||
// the root keeps LayoutNode's ErrorMeasurePolicy ("Undefined measure").
|
// the root keeps LayoutNode's ErrorMeasurePolicy ("Undefined measure").
|
||||||
measurePolicy = androidx.compose.ui.layout.RootMeasurePolicy
|
measurePolicy = androidx.compose.ui.layout.RootMeasurePolicy
|
||||||
}
|
}
|
||||||
private val fOwner = ComposeOwner(rootNode, Density(inDensity), LayoutDirection.Ltr)
|
private val mOwner = ComposeOwner(rootNode, Density(inDensity), LayoutDirection.Ltr)
|
||||||
private val fApplier = NodeApplier(rootNode)
|
private val mApplier = NodeApplier(rootNode)
|
||||||
|
|
||||||
// Upcast to the public supertype so the internal NodeApplier / LayoutNode
|
// Upcast to the public supertype so the internal NodeApplier / LayoutNode
|
||||||
// don't leak across the module boundary; Composition accepts Applier<*>.
|
// don't leak across the module boundary; Composition accepts Applier<*>.
|
||||||
val applier: Applier<*> get() = fApplier
|
val applier: Applier<*> get() = mApplier
|
||||||
|
|
||||||
fun attach() {
|
fun attach() {
|
||||||
// Install the focus root + drag-and-drop root on the root LayoutNode
|
// Install the focus root + drag-and-drop root on the root LayoutNode
|
||||||
@@ -43,8 +43,8 @@ class ComposeRootHost(inDensity: Float = 1f) {
|
|||||||
// focus root the focus tree has no origin and requestFocus never
|
// focus root the focus tree has no origin and requestFocus never
|
||||||
// sticks; without the DnD root the root DragAndDropNode never receives
|
// sticks; without the DnD root the root DragAndDropNode never receives
|
||||||
// events from Sdl3DragAndDropOwner's dispatch calls.
|
// events from Sdl3DragAndDropOwner's dispatch calls.
|
||||||
rootNode.modifier = fOwner.focusOwner.modifier.then(fOwner.dragAndDropManager.modifier)
|
rootNode.modifier = mOwner.focusOwner.modifier.then(mOwner.dragAndDropManager.modifier)
|
||||||
fOwner.attach()
|
mOwner.attach()
|
||||||
// Turn on snapshot observation so writes to `mutableStateOf` (ScrollState.value,
|
// Turn on snapshot observation so writes to `mutableStateOf` (ScrollState.value,
|
||||||
// LazyList firstVisibleItemIndex, etc.) fire the appropriate invalidation
|
// LazyList firstVisibleItemIndex, etc.) fire the appropriate invalidation
|
||||||
// callbacks - requestRelayout / requestRemeasure - routing through the vendored
|
// callbacks - requestRelayout / requestRemeasure - routing through the vendored
|
||||||
@@ -52,53 +52,53 @@ class ComposeRootHost(inDensity: Float = 1f) {
|
|||||||
// state but the placement lambda inside ScrollNode.measure never re-runs, so
|
// state but the placement lambda inside ScrollNode.measure never re-runs, so
|
||||||
// the child layer's move(IntOffset(0, -scroll)) never happens → scroll offset
|
// the child layer's move(IntOffset(0, -scroll)) never happens → scroll offset
|
||||||
// is invisible even though the state is updating.
|
// is invisible even though the state is updating.
|
||||||
fOwner.snapshotObserver.startObserving()
|
mOwner.snapshotObserver.startObserving()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setConstraints(inWidth: Int, inHeight: Int) {
|
fun setConstraints(inWidth: Int, inHeight: Int) {
|
||||||
fOwner.setRootConstraints(Constraints.fixed(inWidth, inHeight))
|
mOwner.setRootConstraints(Constraints.fixed(inWidth, inHeight))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pump the owner's node-animation frame clock (scroll fling / animateScrollToItem / node
|
// Pump the owner's node-animation frame clock (scroll fling / animateScrollToItem / node
|
||||||
// Animatables). Called once per frame by ComposeWindow with a monotonic nanos timestamp.
|
// Animatables). Called once per frame by ComposeWindow with a monotonic nanos timestamp.
|
||||||
fun sendAnimationFrame(inNanos: Long) {
|
fun sendAnimationFrame(inNanos: Long) {
|
||||||
fOwner.animationFrameClock.sendFrame(inNanos)
|
mOwner.animationFrameClock.sendFrame(inNanos)
|
||||||
}
|
}
|
||||||
|
|
||||||
// True while a node-level animation (scroll fling, node Animatable) awaits the next
|
// True while a node-level animation (scroll fling, node Animatable) awaits the next
|
||||||
// animation frame - the node half of the window's quiescence signal (render-to-quiescence
|
// animation frame - the node half of the window's quiescence signal (render-to-quiescence
|
||||||
// screenshot capture asks the window whether ANY work is still pending).
|
// screenshot capture asks the window whether ANY work is still pending).
|
||||||
fun hasAnimationAwaiters(): Boolean = fOwner.animationFrameClock.hasAwaiters
|
fun hasAnimationAwaiters(): Boolean = mOwner.animationFrameClock.hasAwaiters
|
||||||
|
|
||||||
fun measureAndLayout() {
|
fun measureAndLayout() {
|
||||||
// Flush deferred end-of-apply work (focus invalidation etc.) before measuring, so
|
// Flush deferred end-of-apply work (focus invalidation etc.) before measuring, so
|
||||||
// requestFocus() from composition OR from a pointer event this frame takes effect -
|
// requestFocus() from composition OR from a pointer event this frame takes effect -
|
||||||
// e.g. focus-on-click, which schedules a focus invalidation during event dispatch.
|
// e.g. focus-on-click, which schedules a focus invalidation during event dispatch.
|
||||||
fOwner.onEndApplyChanges()
|
mOwner.onEndApplyChanges()
|
||||||
fOwner.measureAndLayout()
|
mOwner.measureAndLayout()
|
||||||
// Drop snapshot observations whose scope is no longer valid (detached nodes,
|
// Drop snapshot observations whose scope is no longer valid (detached nodes,
|
||||||
// destroyed layers, disposed draw scopes). Upstream RootNodeOwner does this after
|
// destroyed layers, disposed draw scopes). Upstream RootNodeOwner does this after
|
||||||
// the measure pass; without it, every disposed subtree's measure/layout/DRAW
|
// the measure pass; without it, every disposed subtree's measure/layout/DRAW
|
||||||
// observation scopes linger in the OwnerSnapshotObserver forever - each pins its
|
// observation scopes linger in the OwnerSnapshotObserver forever - each pins its
|
||||||
// observed object graph (a leak the P2.2 soak caught on ripple/indication draws).
|
// observed object graph (a leak the P2.2 soak caught on ripple/indication draws).
|
||||||
fOwner.snapshotObserver.clearInvalidObservations()
|
mOwner.snapshotObserver.clearInvalidObservations()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw the composed tree into [canvas] - re-records dirty layers, then walks
|
// Draw the composed tree into [canvas] - re-records dirty layers, then walks
|
||||||
// the root so clean layers replay their cached content. Backends call this
|
// the root so clean layers replay their cached content. Backends call this
|
||||||
// instead of rootNode.draw so retained-layer bookkeeping runs each frame.
|
// instead of rootNode.draw so retained-layer bookkeeping runs each frame.
|
||||||
fun drawRoot(canvas: androidx.compose.ui.graphics.Canvas) {
|
fun drawRoot(canvas: androidx.compose.ui.graphics.Canvas) {
|
||||||
fOwner.renderRoot(canvas)
|
mOwner.renderRoot(canvas)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The window wires this to set needsFrame - a layer whose content changed
|
// The window wires this to set needsFrame - a layer whose content changed
|
||||||
// (OwnedLayer.invalidate) schedules a frame even with nothing else pending.
|
// (OwnedLayer.invalidate) schedules a frame even with nothing else pending.
|
||||||
fun setInvalidationCallback(callback: () -> Unit) {
|
fun setInvalidationCallback(callback: () -> Unit) {
|
||||||
fOwner.onInvalidate = callback
|
mOwner.onInvalidate = callback
|
||||||
}
|
}
|
||||||
|
|
||||||
// The owner's FocusOwner IS a FocusManager - the window provides it as LocalFocusManager.
|
// The owner's FocusOwner IS a FocusManager - the window provides it as LocalFocusManager.
|
||||||
val focusManager: androidx.compose.ui.focus.FocusManager get() = fOwner.focusOwner
|
val focusManager: androidx.compose.ui.focus.FocusManager get() = mOwner.focusOwner
|
||||||
|
|
||||||
// ==================
|
// ==================
|
||||||
// MARK: Owner-backed values for the composition-locals seed
|
// MARK: Owner-backed values for the composition-locals seed
|
||||||
@@ -107,19 +107,19 @@ class ComposeRootHost(inDensity: Float = 1f) {
|
|||||||
// Provider; ComposeWindow wraps setContent in a CompositionLocalProvider whose
|
// Provider; ComposeWindow wraps setContent in a CompositionLocalProvider whose
|
||||||
// values come from the ComposeOwner attached below. These accessors expose the
|
// values come from the ComposeOwner attached below. These accessors expose the
|
||||||
// right fields without leaking the `internal Owner` interface across modules.
|
// right fields without leaking the `internal Owner` interface across modules.
|
||||||
val density: androidx.compose.ui.unit.Density get() = fOwner.density
|
val density: androidx.compose.ui.unit.Density get() = mOwner.density
|
||||||
val layoutDirection: androidx.compose.ui.unit.LayoutDirection get() = fOwner.layoutDirection
|
val layoutDirection: androidx.compose.ui.unit.LayoutDirection get() = mOwner.layoutDirection
|
||||||
val viewConfiguration: androidx.compose.ui.platform.ViewConfiguration get() = fOwner.viewConfiguration
|
val viewConfiguration: androidx.compose.ui.platform.ViewConfiguration get() = mOwner.viewConfiguration
|
||||||
val graphicsContext: androidx.compose.ui.graphics.GraphicsContext get() = fOwner.graphicsContext
|
val graphicsContext: androidx.compose.ui.graphics.GraphicsContext get() = mOwner.graphicsContext
|
||||||
val inputModeManager: androidx.compose.ui.input.InputModeManager get() = fOwner.inputModeManager
|
val inputModeManager: androidx.compose.ui.input.InputModeManager get() = mOwner.inputModeManager
|
||||||
val hapticFeedback: androidx.compose.ui.hapticfeedback.HapticFeedback get() = fOwner.hapticFeedBack
|
val hapticFeedback: androidx.compose.ui.hapticfeedback.HapticFeedback get() = mOwner.hapticFeedBack
|
||||||
val textToolbar: androidx.compose.ui.platform.TextToolbar get() = fOwner.textToolbar
|
val textToolbar: androidx.compose.ui.platform.TextToolbar get() = mOwner.textToolbar
|
||||||
val windowInfo: androidx.compose.ui.platform.WindowInfo get() = fOwner.windowInfo
|
val windowInfo: androidx.compose.ui.platform.WindowInfo get() = mOwner.windowInfo
|
||||||
|
|
||||||
// Push SDL window focus into windowInfo.isWindowFocused (focus-reactive UI).
|
// Push SDL window focus into windowInfo.isWindowFocused (focus-reactive UI).
|
||||||
fun setWindowFocused(inFocused: Boolean) = fOwner.setWindowFocused(inFocused)
|
fun setWindowFocused(inFocused: Boolean) = mOwner.setWindowFocused(inFocused)
|
||||||
val softwareKeyboardController: androidx.compose.ui.platform.SoftwareKeyboardController
|
val softwareKeyboardController: androidx.compose.ui.platform.SoftwareKeyboardController
|
||||||
get() = fOwner.softwareKeyboardController
|
get() = mOwner.softwareKeyboardController
|
||||||
|
|
||||||
// PointerIconService and its composition local are :ui-internal, so ComposeWindow
|
// PointerIconService and its composition local are :ui-internal, so ComposeWindow
|
||||||
// can't provide the local directly. This wrapper seeds it from the owner so
|
// can't provide the local directly. This wrapper seeds it from the owner so
|
||||||
@@ -127,7 +127,7 @@ class ComposeRootHost(inDensity: Float = 1f) {
|
|||||||
@androidx.compose.runtime.Composable
|
@androidx.compose.runtime.Composable
|
||||||
fun ProvidePointerIconService(content: @androidx.compose.runtime.Composable () -> Unit) {
|
fun ProvidePointerIconService(content: @androidx.compose.runtime.Composable () -> Unit) {
|
||||||
androidx.compose.runtime.CompositionLocalProvider(
|
androidx.compose.runtime.CompositionLocalProvider(
|
||||||
androidx.compose.ui.platform.LocalPointerIconService provides fOwner.pointerIconService,
|
androidx.compose.ui.platform.LocalPointerIconService provides mOwner.pointerIconService,
|
||||||
content = content,
|
content = content,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -140,7 +140,7 @@ class ComposeRootHost(inDensity: Float = 1f) {
|
|||||||
onKeyEvent) - drives text-field editing keys (backspace / arrows / enter) and
|
onKeyEvent) - drives text-field editing keys (backspace / arrows / enter) and
|
||||||
clickable Enter/Space activation. Returns true if some node consumed it. */
|
clickable Enter/Space activation. Returns true if some node consumed it. */
|
||||||
fun dispatchKeyEvent(inEvent: androidx.compose.ui.input.key.KeyEvent): Boolean =
|
fun dispatchKeyEvent(inEvent: androidx.compose.ui.input.key.KeyEvent): Boolean =
|
||||||
runCatching { fOwner.focusOwner.dispatchKeyEvent(inEvent) }.getOrDefault(false)
|
runCatching { mOwner.focusOwner.dispatchKeyEvent(inEvent) }.getOrDefault(false)
|
||||||
|
|
||||||
// ==================
|
// ==================
|
||||||
// MARK: Input - feed the vendored PointerInputEventProcessor
|
// MARK: Input - feed the vendored PointerInputEventProcessor
|
||||||
@@ -152,13 +152,13 @@ class ComposeRootHost(inDensity: Float = 1f) {
|
|||||||
// `expect PointerInputEvent` has no commonMain constructor, so the actual build+dispatch
|
// `expect PointerInputEvent` has no commonMain constructor, so the actual build+dispatch
|
||||||
// lives in nativeMain (feedPointerToProcessor).
|
// lives in nativeMain (feedPointerToProcessor).
|
||||||
fun onPointerRaw(inX: Float, inY: Float, inType: Int, inButton: Int, inUptime: Long) {
|
fun onPointerRaw(inX: Float, inY: Float, inType: Int, inButton: Int, inUptime: Long) {
|
||||||
feedPointerToProcessor(fOwner, inType, inButton, inUptime, inX, inY)
|
feedPointerToProcessor(mOwner, inType, inButton, inUptime, inX, inY)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mouse wheel - feed a scroll PointerInputEvent to the processor so the vendored
|
// Mouse wheel - feed a scroll PointerInputEvent to the processor so the vendored
|
||||||
// Modifier.scrollable (MouseWheelScrollingLogic) handles it, exactly like upstream.
|
// Modifier.scrollable (MouseWheelScrollingLogic) handles it, exactly like upstream.
|
||||||
fun onWheel(inX: Float, inY: Float, inDeltaX: Float, inDeltaY: Float, inUptime: Long) {
|
fun onWheel(inX: Float, inY: Float, inDeltaX: Float, inDeltaY: Float, inUptime: Long) {
|
||||||
feedScrollToProcessor(fOwner, inX, inY, inDeltaX, inDeltaY, inUptime)
|
feedScrollToProcessor(mOwner, inX, inY, inDeltaX, inDeltaY, inUptime)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================
|
// ==================
|
||||||
@@ -169,11 +169,11 @@ class ComposeRootHost(inDensity: Float = 1f) {
|
|||||||
// COMPLETE to these methods; Sdl3DragAndDropOwner accumulates the drop
|
// COMPLETE to these methods; Sdl3DragAndDropOwner accumulates the drop
|
||||||
// session and dispatches through the root DragAndDropNode on COMPLETE.
|
// session and dispatches through the root DragAndDropNode on COMPLETE.
|
||||||
|
|
||||||
fun onDropBegin() = fOwner.dragAndDropManager.dropBegin()
|
fun onDropBegin() = mOwner.dragAndDropManager.dropBegin()
|
||||||
fun onDropPosition(inX: Float, inY: Float) = fOwner.dragAndDropManager.dropPosition(inX, inY)
|
fun onDropPosition(inX: Float, inY: Float) = mOwner.dragAndDropManager.dropPosition(inX, inY)
|
||||||
fun onDropFile(inPath: String) = fOwner.dragAndDropManager.dropFile(inPath)
|
fun onDropFile(inPath: String) = mOwner.dragAndDropManager.dropFile(inPath)
|
||||||
fun onDropText(inText: String) = fOwner.dragAndDropManager.dropText(inText)
|
fun onDropText(inText: String) = mOwner.dragAndDropManager.dropText(inText)
|
||||||
fun onDropComplete() = fOwner.dragAndDropManager.dropComplete()
|
fun onDropComplete() = mOwner.dragAndDropManager.dropComplete()
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: feedPointerToProcessor / feedScrollToProcessor are plain nativeMain
|
// NOTE: feedPointerToProcessor / feedScrollToProcessor are plain nativeMain
|
||||||
|
|||||||
+9
-9
@@ -18,9 +18,9 @@ import com.compose.sdl.node.impl.ComposeOwner
|
|||||||
mouse down unless `buttons.isPrimaryPressed`, and drag/scroll gestures need the held button to
|
mouse down unless `buttons.isPrimaryPressed`, and drag/scroll gestures need the held button to
|
||||||
persist across Move events, so the synthesized PointerInputEvent must carry a live button mask -
|
persist across Move events, so the synthesized PointerInputEvent must carry a live button mask -
|
||||||
not just the changed button. A single mouse => module-level flags are enough. */
|
not just the changed button. A single mouse => module-level flags are enough. */
|
||||||
private var fPrimaryDown = false
|
private var mPrimaryDown = false
|
||||||
private var fSecondaryDown = false
|
private var mSecondaryDown = false
|
||||||
private var fTertiaryDown = false
|
private var mTertiaryDown = false
|
||||||
|
|
||||||
/** Builds the internal PointerInputEvent (its constructor is native-only) from a single mouse
|
/** Builds the internal PointerInputEvent (its constructor is native-only) from a single mouse
|
||||||
pointer and drives the owner's PointerInputEventProcessor. inType: 0=Move 1=Press 2=Release 3=Exit;
|
pointer and drives the owner's PointerInputEventProcessor. inType: 0=Move 1=Press 2=Release 3=Exit;
|
||||||
@@ -34,14 +34,14 @@ internal fun feedPointerToProcessor(
|
|||||||
inY: Float,
|
inY: Float,
|
||||||
) {
|
) {
|
||||||
when (inType) {
|
when (inType) {
|
||||||
1 -> when (inButton) { 0 -> fPrimaryDown = true; 1 -> fSecondaryDown = true; 2 -> fTertiaryDown = true }
|
1 -> when (inButton) { 0 -> mPrimaryDown = true; 1 -> mSecondaryDown = true; 2 -> mTertiaryDown = true }
|
||||||
2 -> when (inButton) { 0 -> fPrimaryDown = false; 1 -> fSecondaryDown = false; 2 -> fTertiaryDown = false }
|
2 -> when (inButton) { 0 -> mPrimaryDown = false; 1 -> mSecondaryDown = false; 2 -> mTertiaryDown = false }
|
||||||
}
|
}
|
||||||
|
|
||||||
val vButtons = PointerButtons(
|
val vButtons = PointerButtons(
|
||||||
isPrimaryPressed = fPrimaryDown,
|
isPrimaryPressed = mPrimaryDown,
|
||||||
isSecondaryPressed = fSecondaryDown,
|
isSecondaryPressed = mSecondaryDown,
|
||||||
isTertiaryPressed = fTertiaryDown,
|
isTertiaryPressed = mTertiaryDown,
|
||||||
)
|
)
|
||||||
val vButton = if (inType == 1 || inType == 2) {
|
val vButton = if (inType == 1 || inType == 2) {
|
||||||
when (inButton) {
|
when (inButton) {
|
||||||
@@ -57,7 +57,7 @@ internal fun feedPointerToProcessor(
|
|||||||
uptime = inUptime,
|
uptime = inUptime,
|
||||||
positionOnScreen = vPos,
|
positionOnScreen = vPos,
|
||||||
position = vPos,
|
position = vPos,
|
||||||
down = fPrimaryDown || fSecondaryDown || fTertiaryDown,
|
down = mPrimaryDown || mSecondaryDown || mTertiaryDown,
|
||||||
pressure = 1f,
|
pressure = 1f,
|
||||||
type = PointerType.Mouse,
|
type = PointerType.Mouse,
|
||||||
activeHover = inType == 0, // Exit (3) also reports no active hover
|
activeHover = inType == 0, // Exit (3) also reports no active hover
|
||||||
|
|||||||
@@ -80,28 +80,28 @@ internal class ComposeOwner(
|
|||||||
) : Owner, OwnedLayerManager {
|
) : Owner, OwnedLayerManager {
|
||||||
|
|
||||||
// The vendored measure/layout state machine, rooted at [root].
|
// The vendored measure/layout state machine, rooted at [root].
|
||||||
private val fDelegate = MeasureAndLayoutDelegate(root)
|
private val mDelegate = MeasureAndLayoutDelegate(root)
|
||||||
|
|
||||||
// The vendored pointer-input dispatcher. Routes a PointerInputEvent through
|
// The vendored pointer-input dispatcher. Routes a PointerInputEvent through
|
||||||
// HitPathTracker to the tree's PointerInputModifierNodes (hover / gesture),
|
// HitPathTracker to the tree's PointerInputModifierNodes (hover / gesture),
|
||||||
// synthesizing Enter/Exit. Fed from ComposeWindow via ComposeRootHost.
|
// synthesizing Enter/Exit. Fed from ComposeWindow via ComposeRootHost.
|
||||||
private val fPointerProcessor = androidx.compose.ui.input.pointer.PointerInputEventProcessor(root)
|
private val mPointerProcessor = androidx.compose.ui.input.pointer.PointerInputEventProcessor(root)
|
||||||
|
|
||||||
// Window focus + container size backing [windowInfo], snapshot-backed so
|
// Window focus + container size backing [windowInfo], snapshot-backed so
|
||||||
// focus-reactive UI and LocalWindowInfo.containerSize readers recompose. Fed by
|
// focus-reactive UI and LocalWindowInfo.containerSize readers recompose. Fed by
|
||||||
// the window: focus via setWindowFocused (SDL activation), size from setRootConstraints.
|
// the window: focus via setWindowFocused (SDL activation), size from setRootConstraints.
|
||||||
private val fWindowFocused = androidx.compose.runtime.mutableStateOf(true)
|
private val mWindowFocused = androidx.compose.runtime.mutableStateOf(true)
|
||||||
private val fContainerSize = androidx.compose.runtime.mutableStateOf(androidx.compose.ui.unit.IntSize.Zero)
|
private val mContainerSize = androidx.compose.runtime.mutableStateOf(androidx.compose.ui.unit.IntSize.Zero)
|
||||||
|
|
||||||
// Called by the window on SDL focus gain/loss.
|
// Called by the window on SDL focus gain/loss.
|
||||||
internal fun setWindowFocused(inFocused: Boolean) { fWindowFocused.value = inFocused }
|
internal fun setWindowFocused(inFocused: Boolean) { mWindowFocused.value = inFocused }
|
||||||
|
|
||||||
// Dispatch one pointer event to the upstream PointerInputModifierNode tree.
|
// Dispatch one pointer event to the upstream PointerInputModifierNode tree.
|
||||||
// [this] is the PositionCalculator (Owner : PositionCalculator).
|
// [this] is the PositionCalculator (Owner : PositionCalculator).
|
||||||
internal fun processPointerInput(inEvent: androidx.compose.ui.input.pointer.PointerInputEvent): Boolean {
|
internal fun processPointerInput(inEvent: androidx.compose.ui.input.pointer.PointerInputEvent): Boolean {
|
||||||
val vResult = fPointerProcessor.process(inEvent, this)
|
val vResult = mPointerProcessor.process(inEvent, this)
|
||||||
// Apply the cursor the hover pipeline just set for this event (deduped in SdlCursors).
|
// Apply the cursor the hover pipeline just set for this event (deduped in SdlCursors).
|
||||||
com.compose.sdl.SdlCursors.apply(fPointerIcon)
|
com.compose.sdl.SdlCursors.apply(mPointerIcon)
|
||||||
return vResult.dispatchedToAPointerInputModifier
|
return vResult.dispatchedToAPointerInputModifier
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,11 +115,11 @@ internal class ComposeOwner(
|
|||||||
// Sets the constraints the root is measured against (window pixel size in
|
// Sets the constraints the root is measured against (window pixel size in
|
||||||
// logical points). Call each frame before [measureAndLayout].
|
// logical points). Call each frame before [measureAndLayout].
|
||||||
fun setRootConstraints(inConstraints: Constraints) {
|
fun setRootConstraints(inConstraints: Constraints) {
|
||||||
fDelegate.updateRootConstraints(inConstraints)
|
mDelegate.updateRootConstraints(inConstraints)
|
||||||
// Report the container size (physical px, the port's layout coord space) to
|
// Report the container size (physical px, the port's layout coord space) to
|
||||||
// windowInfo; updates on every resize since the window re-measures per frame.
|
// windowInfo; updates on every resize since the window re-measures per frame.
|
||||||
if (inConstraints.hasBoundedWidth && inConstraints.hasBoundedHeight) {
|
if (inConstraints.hasBoundedWidth && inConstraints.hasBoundedHeight) {
|
||||||
fContainerSize.value =
|
mContainerSize.value =
|
||||||
androidx.compose.ui.unit.IntSize(inConstraints.maxWidth, inConstraints.maxHeight)
|
androidx.compose.ui.unit.IntSize(inConstraints.maxWidth, inConstraints.maxHeight)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,8 +138,8 @@ internal class ComposeOwner(
|
|||||||
forceRequest: Boolean,
|
forceRequest: Boolean,
|
||||||
scheduleMeasureAndLayout: Boolean,
|
scheduleMeasureAndLayout: Boolean,
|
||||||
) {
|
) {
|
||||||
if (affectsLookahead) fDelegate.requestLookaheadRemeasure(layoutNode, forceRequest)
|
if (affectsLookahead) mDelegate.requestLookaheadRemeasure(layoutNode, forceRequest)
|
||||||
else fDelegate.requestRemeasure(layoutNode, forceRequest)
|
else mDelegate.requestRemeasure(layoutNode, forceRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRequestRelayout(
|
override fun onRequestRelayout(
|
||||||
@@ -147,25 +147,25 @@ internal class ComposeOwner(
|
|||||||
affectsLookahead: Boolean,
|
affectsLookahead: Boolean,
|
||||||
forceRequest: Boolean,
|
forceRequest: Boolean,
|
||||||
) {
|
) {
|
||||||
if (affectsLookahead) fDelegate.requestLookaheadRelayout(layoutNode, forceRequest)
|
if (affectsLookahead) mDelegate.requestLookaheadRelayout(layoutNode, forceRequest)
|
||||||
else fDelegate.requestRelayout(layoutNode, forceRequest)
|
else mDelegate.requestRelayout(layoutNode, forceRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun measureAndLayout(sendPointerUpdate: Boolean) {
|
override fun measureAndLayout(sendPointerUpdate: Boolean) {
|
||||||
fDelegate.measureAndLayout()
|
mDelegate.measureAndLayout()
|
||||||
fDelegate.dispatchOnPositionedCallbacks()
|
mDelegate.dispatchOnPositionedCallbacks()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun measureAndLayout(layoutNode: LayoutNode, constraints: Constraints) {
|
override fun measureAndLayout(layoutNode: LayoutNode, constraints: Constraints) {
|
||||||
fDelegate.measureAndLayout(layoutNode, constraints)
|
mDelegate.measureAndLayout(layoutNode, constraints)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun forceMeasureTheSubtree(layoutNode: LayoutNode, affectsLookahead: Boolean) {
|
override fun forceMeasureTheSubtree(layoutNode: LayoutNode, affectsLookahead: Boolean) {
|
||||||
fDelegate.forceMeasureTheSubtree(layoutNode, affectsLookahead)
|
mDelegate.forceMeasureTheSubtree(layoutNode, affectsLookahead)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDetach(node: LayoutNode) {
|
override fun onDetach(node: LayoutNode) {
|
||||||
fDelegate.onNodeDetached(node)
|
mDelegate.onNodeDetached(node)
|
||||||
// Drop the detached node's read-observation scopes, exactly as upstream
|
// Drop the detached node's read-observation scopes, exactly as upstream
|
||||||
// RootNodeOwner.onDetach does. Without this, every disposed node's measure/
|
// RootNodeOwner.onDetach does. Without this, every disposed node's measure/
|
||||||
// layout/draw observation scopes accumulate in the snapshot observer forever
|
// layout/draw observation scopes accumulate in the snapshot observer forever
|
||||||
@@ -174,7 +174,7 @@ internal class ComposeOwner(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun registerOnLayoutCompletedListener(listener: Owner.OnLayoutCompletedListener) {
|
override fun registerOnLayoutCompletedListener(listener: Owner.OnLayoutCompletedListener) {
|
||||||
fDelegate.registerOnLayoutCompletedListener(listener)
|
mDelegate.registerOnLayoutCompletedListener(listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun requestOnPositionedCallback(layoutNode: LayoutNode) = Unit
|
override fun requestOnPositionedCallback(layoutNode: LayoutNode) = Unit
|
||||||
@@ -345,12 +345,12 @@ internal class ComposeOwner(
|
|||||||
}
|
}
|
||||||
// Desired hover cursor, set by the vendored HoverIconModifierNode
|
// Desired hover cursor, set by the vendored HoverIconModifierNode
|
||||||
// (Modifier.pointerHoverIcon) and applied via SDL in processPointerInput. null = default.
|
// (Modifier.pointerHoverIcon) and applied via SDL in processPointerInput. null = default.
|
||||||
private var fPointerIcon: androidx.compose.ui.input.pointer.PointerIcon =
|
private var mPointerIcon: androidx.compose.ui.input.pointer.PointerIcon =
|
||||||
androidx.compose.ui.input.pointer.PointerIcon.Default
|
androidx.compose.ui.input.pointer.PointerIcon.Default
|
||||||
override val pointerIconService: PointerIconService = object : PointerIconService {
|
override val pointerIconService: PointerIconService = object : PointerIconService {
|
||||||
override fun getIcon(): androidx.compose.ui.input.pointer.PointerIcon = fPointerIcon
|
override fun getIcon(): androidx.compose.ui.input.pointer.PointerIcon = mPointerIcon
|
||||||
override fun setIcon(value: androidx.compose.ui.input.pointer.PointerIcon?) {
|
override fun setIcon(value: androidx.compose.ui.input.pointer.PointerIcon?) {
|
||||||
fPointerIcon = value ?: androidx.compose.ui.input.pointer.PointerIcon.Default
|
mPointerIcon = value ?: androidx.compose.ui.input.pointer.PointerIcon.Default
|
||||||
}
|
}
|
||||||
override fun getStylusHoverIcon(): androidx.compose.ui.input.pointer.PointerIcon? = null
|
override fun getStylusHoverIcon(): androidx.compose.ui.input.pointer.PointerIcon? = null
|
||||||
override fun setStylusHoverIcon(value: androidx.compose.ui.input.pointer.PointerIcon?) {}
|
override fun setStylusHoverIcon(value: androidx.compose.ui.input.pointer.PointerIcon?) {}
|
||||||
@@ -368,13 +368,13 @@ internal class ComposeOwner(
|
|||||||
owner = this,
|
owner = this,
|
||||||
)
|
)
|
||||||
override val windowInfo: WindowInfo = object : WindowInfo {
|
override val windowInfo: WindowInfo = object : WindowInfo {
|
||||||
override val isWindowFocused: Boolean get() = fWindowFocused.value
|
override val isWindowFocused: Boolean get() = mWindowFocused.value
|
||||||
override val containerSize: androidx.compose.ui.unit.IntSize get() = fContainerSize.value
|
override val containerSize: androidx.compose.ui.unit.IntSize get() = mContainerSize.value
|
||||||
override val containerDpSize: androidx.compose.ui.unit.DpSize
|
override val containerDpSize: androidx.compose.ui.unit.DpSize
|
||||||
get() = with(density) {
|
get() = with(density) {
|
||||||
androidx.compose.ui.unit.DpSize(
|
androidx.compose.ui.unit.DpSize(
|
||||||
fContainerSize.value.width.toDp(),
|
mContainerSize.value.width.toDp(),
|
||||||
fContainerSize.value.height.toDp(),
|
mContainerSize.value.height.toDp(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -412,16 +412,16 @@ internal class ComposeOwner(
|
|||||||
// End-of-apply-changes listeners - the focus system (FocusInvalidationManager) and other
|
// End-of-apply-changes listeners - the focus system (FocusInvalidationManager) and other
|
||||||
// engine parts register here to defer work until changes are applied; without this the
|
// engine parts register here to defer work until changes are applied; without this the
|
||||||
// focus invalidation never flushes and requestFocus() (e.g. focus-on-click) never sticks.
|
// focus invalidation never flushes and requestFocus() (e.g. focus-on-click) never sticks.
|
||||||
private val fEndApplyChangesListeners = mutableListOf<() -> Unit>()
|
private val mEndApplyChangesListeners = mutableListOf<() -> Unit>()
|
||||||
|
|
||||||
override fun registerOnEndApplyChangesListener(listener: () -> Unit) {
|
override fun registerOnEndApplyChangesListener(listener: () -> Unit) {
|
||||||
if (listener !in fEndApplyChangesListeners) fEndApplyChangesListeners.add(listener)
|
if (listener !in mEndApplyChangesListeners) mEndApplyChangesListeners.add(listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onEndApplyChanges() {
|
override fun onEndApplyChanges() {
|
||||||
// Drain in order; listeners may re-register, so loop until empty.
|
// Drain in order; listeners may re-register, so loop until empty.
|
||||||
while (fEndApplyChangesListeners.isNotEmpty()) {
|
while (mEndApplyChangesListeners.isNotEmpty()) {
|
||||||
val vListener = fEndApplyChangesListeners.removeAt(0)
|
val vListener = mEndApplyChangesListeners.removeAt(0)
|
||||||
vListener()
|
vListener()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,10 +17,10 @@ import com.compose.sdl.loadComposeResourceBytes
|
|||||||
*
|
*
|
||||||
* Idempotent; called from ComposeWindow.installGlobals (data.kres is loadable by then).
|
* Idempotent; called from ComposeWindow.installGlobals (data.kres is loadable by then).
|
||||||
*/
|
*/
|
||||||
private var fRegistered = false
|
private var mRegistered = false
|
||||||
|
|
||||||
fun registerGenericFonts() {
|
fun registerGenericFonts() {
|
||||||
if (fRegistered) return
|
if (mRegistered) return
|
||||||
fRegistered = true
|
mRegistered = true
|
||||||
loadComposeResourceBytes("font/NotoSansMono.ttf")?.let { IconFont.register("generic:monospace", it) }
|
loadComposeResourceBytes("font/NotoSansMono.ttf")?.let { IconFont.register("generic:monospace", it) }
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -6,4 +6,4 @@ import com.compose.sdl.renderer.skia.SkiaBridge
|
|||||||
// MARK: Linux GPU defaults
|
// MARK: Linux GPU defaults
|
||||||
// ==================
|
// ==================
|
||||||
|
|
||||||
internal actual fun makeMetalBridge(backend: SDL3Backend): SkiaBridge? = null
|
internal actual fun makeMetalBridge(backend: Sdl3Backend): SkiaBridge? = null
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@ package com.compose.sdl
|
|||||||
import com.compose.sdl.renderer.skia.SkiaBridge
|
import com.compose.sdl.renderer.skia.SkiaBridge
|
||||||
import com.compose.sdl.renderer.skia.SkiaMetalBridge
|
import com.compose.sdl.renderer.skia.SkiaMetalBridge
|
||||||
|
|
||||||
internal actual fun makeMetalBridge(backend: SDL3Backend): SkiaBridge? {
|
internal actual fun makeMetalBridge(backend: Sdl3Backend): SkiaBridge? {
|
||||||
val bridge = SkiaMetalBridge(backend)
|
val bridge = SkiaMetalBridge(backend)
|
||||||
return if (bridge.init()) bridge else null
|
return if (bridge.init()) bridge else null
|
||||||
}
|
}
|
||||||
+43
-43
@@ -37,16 +37,16 @@ import sdl3.SDL_Metal_GetLayer
|
|||||||
per frame, so we tear the BackendRenderTarget + Surface down every frame
|
per frame, so we tear the BackendRenderTarget + Surface down every frame
|
||||||
(cheap - the DirectContext + device + queue persist). */
|
(cheap - the DirectContext + device + queue persist). */
|
||||||
@OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
|
@OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
|
||||||
internal class SkiaMetalBridge(private val backend: SDL3Backend) : SkiaBridge {
|
internal class SkiaMetalBridge(private val backend: Sdl3Backend) : SkiaBridge {
|
||||||
private var fDevice: MTLDeviceProtocol? = null
|
private var mDevice: MTLDeviceProtocol? = null
|
||||||
private var fQueue: MTLCommandQueueProtocol? = null
|
private var mQueue: MTLCommandQueueProtocol? = null
|
||||||
private var fLayer: CAMetalLayer? = null
|
private var mLayer: CAMetalLayer? = null
|
||||||
private var fContext: DirectContext? = null
|
private var mContext: DirectContext? = null
|
||||||
private var fSurface: Surface? = null
|
private var mSurface: Surface? = null
|
||||||
private var fRT: BackendRenderTarget? = null
|
private var mRT: BackendRenderTarget? = null
|
||||||
private var fDrawable: CAMetalDrawableProtocol? = null
|
private var mDrawable: CAMetalDrawableProtocol? = null
|
||||||
private var fWidth = 0
|
private var mWidth = 0
|
||||||
private var fHeight = 0
|
private var mHeight = 0
|
||||||
|
|
||||||
fun init(): Boolean {
|
fun init(): Boolean {
|
||||||
val vView = backend.metalView ?: run {
|
val vView = backend.metalView ?: run {
|
||||||
@@ -84,22 +84,22 @@ internal class SkiaMetalBridge(private val backend: SDL3Backend) : SkiaBridge {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
fDevice = vDevice
|
mDevice = vDevice
|
||||||
fQueue = vQueue
|
mQueue = vQueue
|
||||||
fLayer = vLayer
|
mLayer = vLayer
|
||||||
fContext = vContext
|
mContext = vContext
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override val canvas: Canvas
|
override val canvas: Canvas
|
||||||
get() = requireNotNull(fSurface) { "SkiaMetalBridge not initialised" }.canvas
|
get() = requireNotNull(mSurface) { "SkiaMetalBridge not initialised" }.canvas
|
||||||
|
|
||||||
override fun ensureSize(inWidth: Int, inHeight: Int): Boolean {
|
override fun ensureSize(inWidth: Int, inHeight: Int): Boolean {
|
||||||
if (inWidth <= 0 || inHeight <= 0) return false
|
if (inWidth <= 0 || inHeight <= 0) return false
|
||||||
val vLayer = fLayer ?: return false
|
val vLayer = mLayer ?: return false
|
||||||
val vContext = fContext ?: return false
|
val vContext = mContext ?: return false
|
||||||
|
|
||||||
if (inWidth != fWidth || inHeight != fHeight) {
|
if (inWidth != mWidth || inHeight != mHeight) {
|
||||||
vLayer.drawableSize = CGSizeMake(inWidth.toDouble(), inHeight.toDouble())
|
vLayer.drawableSize = CGSizeMake(inWidth.toDouble(), inHeight.toDouble())
|
||||||
// Keep contentsScale in lock-step with the live backing density.
|
// Keep contentsScale in lock-step with the live backing density.
|
||||||
// Setting it only in init() can leave it stale at 1.0 when the
|
// Setting it only in init() can leave it stale at 1.0 when the
|
||||||
@@ -107,18 +107,18 @@ internal class SkiaMetalBridge(private val backend: SDL3Backend) : SkiaBridge {
|
|||||||
// drawable then gets downscaled to the layer's logical backing and
|
// drawable then gets downscaled to the layer's logical backing and
|
||||||
// re-upscaled by the display, softening text and aliasing edges.
|
// re-upscaled by the display, softening text and aliasing edges.
|
||||||
vLayer.contentsScale = backend.pixelDensity.toDouble()
|
vLayer.contentsScale = backend.pixelDensity.toDouble()
|
||||||
fWidth = inWidth
|
mWidth = inWidth
|
||||||
fHeight = inHeight
|
mHeight = inHeight
|
||||||
println("SkiaMetalBridge: drawable ${inWidth}x${inHeight} @ contentsScale ${backend.pixelDensity}")
|
println("SkiaMetalBridge: drawable ${inWidth}x${inHeight} @ contentsScale ${backend.pixelDensity}")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Per-frame: drop the previous frame's drawable wrappers and grab a
|
// Per-frame: drop the previous frame's drawable wrappers and grab a
|
||||||
// fresh one. nextDrawable() may block (~16ms vsync) when 3 drawables
|
// fresh one. nextDrawable() may block (~16ms vsync) when 3 drawables
|
||||||
// are already in flight - that's the natural pacing mechanism.
|
// are already in flight - that's the natural pacing mechanism.
|
||||||
fSurface?.close()
|
mSurface?.close()
|
||||||
fRT?.close()
|
mRT?.close()
|
||||||
fSurface = null
|
mSurface = null
|
||||||
fRT = null
|
mRT = null
|
||||||
|
|
||||||
val vDrawable = vLayer.nextDrawable() ?: run {
|
val vDrawable = vLayer.nextDrawable() ?: run {
|
||||||
println("SkiaMetalBridge: nextDrawable returned null")
|
println("SkiaMetalBridge: nextDrawable returned null")
|
||||||
@@ -142,38 +142,38 @@ internal class SkiaMetalBridge(private val backend: SDL3Backend) : SkiaBridge {
|
|||||||
println("SkiaMetalBridge: Surface.makeFromBackendRenderTarget returned null")
|
println("SkiaMetalBridge: Surface.makeFromBackendRenderTarget returned null")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
fRT = vRT
|
mRT = vRT
|
||||||
fSurface = vSurface
|
mSurface = vSurface
|
||||||
fDrawable = vDrawable
|
mDrawable = vDrawable
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun present() {
|
override fun present() {
|
||||||
val vSurface = fSurface ?: return
|
val vSurface = mSurface ?: return
|
||||||
val vQueue = fQueue ?: return
|
val vQueue = mQueue ?: return
|
||||||
val vDrawable = fDrawable ?: return
|
val vDrawable = mDrawable ?: return
|
||||||
vSurface.flushAndSubmit()
|
vSurface.flushAndSubmit()
|
||||||
val vCommandBuffer = vQueue.commandBuffer() ?: return
|
val vCommandBuffer = vQueue.commandBuffer() ?: return
|
||||||
vCommandBuffer.presentDrawable(vDrawable)
|
vCommandBuffer.presentDrawable(vDrawable)
|
||||||
vCommandBuffer.commit()
|
vCommandBuffer.commit()
|
||||||
fDrawable = null
|
mDrawable = null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun snapshot(): Image? = fSurface?.makeImageSnapshot()
|
override fun snapshot(): Image? = mSurface?.makeImageSnapshot()
|
||||||
|
|
||||||
override fun snapshotBgra(): Triple<Int, Int, ByteArray>? =
|
override fun snapshotBgra(): Triple<Int, Int, ByteArray>? =
|
||||||
readBackBgra(fSurface, fWidth, fHeight)
|
readBackBgra(mSurface, mWidth, mHeight)
|
||||||
|
|
||||||
override fun destroy() {
|
override fun destroy() {
|
||||||
fSurface?.close()
|
mSurface?.close()
|
||||||
fRT?.close()
|
mRT?.close()
|
||||||
fContext?.close()
|
mContext?.close()
|
||||||
fSurface = null
|
mSurface = null
|
||||||
fRT = null
|
mRT = null
|
||||||
fContext = null
|
mContext = null
|
||||||
fLayer = null
|
mLayer = null
|
||||||
fQueue = null
|
mQueue = null
|
||||||
fDevice = null
|
mDevice = null
|
||||||
fDrawable = null
|
mDrawable = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,17 +7,17 @@ import kotlin.experimental.ExperimentalNativeApi
|
|||||||
// MARK: Metal bridge factory (skia-only)
|
// MARK: Metal bridge factory (skia-only)
|
||||||
// ==================
|
// ==================
|
||||||
|
|
||||||
internal expect fun makeMetalBridge(backend: SDL3Backend): SkiaBridge?
|
internal expect fun makeMetalBridge(backend: Sdl3Backend): SkiaBridge?
|
||||||
|
|
||||||
actual fun rendererPreferredGpuMode(): GpuMode {
|
actual fun rendererPreferredGpuMode(): GpuMode {
|
||||||
@OptIn(ExperimentalNativeApi::class)
|
@OptIn(ExperimentalNativeApi::class)
|
||||||
return when (Platform.osFamily) {
|
return when (Platform.osFamily) {
|
||||||
OsFamily.MACOSX -> GpuMode.Skia.Metal
|
OsFamily.MACOSX -> GpuMode.Metal
|
||||||
OsFamily.LINUX -> GpuMode.Skia.OpenGL
|
OsFamily.LINUX -> GpuMode.OpenGL
|
||||||
// Windows (mingwX64, Route 1a): GPU via Skia's GL backend on an SDL WGL
|
// Windows (mingwX64, Route 1a): GPU via Skia's GL backend on an SDL WGL
|
||||||
// context (SkiaGLBridge, shared with Linux). CPU raster stays available
|
// context (SkiaGLBridge, shared with Linux). CPU raster stays available
|
||||||
// as --gpu=software. (A native D3D12 bridge is the milestone-2 upgrade.)
|
// as --gpu=software. (A native D3D12 bridge is the milestone-2 upgrade.)
|
||||||
OsFamily.WINDOWS -> GpuMode.Skia.OpenGL
|
OsFamily.WINDOWS -> GpuMode.OpenGL
|
||||||
else -> GpuMode.Software
|
else -> GpuMode.CpuRaster
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@ import com.compose.sdl.renderer.skia.SkiaRenderBackend
|
|||||||
actual delegates here when Skia is the selected renderer). Mirrors the
|
actual delegates here when Skia is the selected renderer). Mirrors the
|
||||||
SDL3 module's createRenderBackend / rendererPreferredGpuMode in the same
|
SDL3 module's createRenderBackend / rendererPreferredGpuMode in the same
|
||||||
package; exactly one renderer module is included per target. */
|
package; exactly one renderer module is included per target. */
|
||||||
actual fun createRenderBackend(inSdl: SDL3Backend, inGpu: GpuMode): RenderBackend? {
|
actual fun createRenderBackend(inSdl: Sdl3Backend, inGpu: GpuMode): RenderBackend? {
|
||||||
val vResolved = if (inGpu is GpuMode.Auto) rendererPreferredGpuMode() else inGpu
|
val vResolved = if (inGpu is GpuMode.Auto) rendererPreferredGpuMode() else inGpu
|
||||||
return try {
|
return try {
|
||||||
SkiaRenderBackend(inSdl, vResolved)
|
SkiaRenderBackend(inSdl, vResolved)
|
||||||
|
|||||||
+1
-1
@@ -13,7 +13,7 @@ import org.jetbrains.skia.Image
|
|||||||
per frame; the concrete implementation owns whatever GPU / CPU
|
per frame; the concrete implementation owns whatever GPU / CPU
|
||||||
resources it needs (raster buffer + SDL_Texture, or a GL/Metal backend
|
resources it needs (raster buffer + SDL_Texture, or a GL/Metal backend
|
||||||
render target). */
|
render target). */
|
||||||
interface SkiaBridge {
|
internal interface SkiaBridge {
|
||||||
val canvas: Canvas
|
val canvas: Canvas
|
||||||
fun ensureSize(inWidth: Int, inHeight: Int): Boolean
|
fun ensureSize(inWidth: Int, inHeight: Int): Boolean
|
||||||
fun present()
|
fun present()
|
||||||
|
|||||||
+57
-32
@@ -4,16 +4,13 @@ import com.compose.sdl.*
|
|||||||
|
|
||||||
import kotlinx.cinterop.*
|
import kotlinx.cinterop.*
|
||||||
import org.jetbrains.skia.BackendRenderTarget
|
import org.jetbrains.skia.BackendRenderTarget
|
||||||
import org.jetbrains.skia.Bitmap
|
|
||||||
import org.jetbrains.skia.Canvas
|
import org.jetbrains.skia.Canvas
|
||||||
import org.jetbrains.skia.ColorAlphaType
|
|
||||||
import org.jetbrains.skia.ColorType
|
|
||||||
import org.jetbrains.skia.DirectContext
|
import org.jetbrains.skia.DirectContext
|
||||||
import org.jetbrains.skia.Image
|
import org.jetbrains.skia.Image
|
||||||
import org.jetbrains.skia.ImageInfo
|
|
||||||
import org.jetbrains.skia.Surface
|
import org.jetbrains.skia.Surface
|
||||||
import org.jetbrains.skia.SurfaceColorFormat
|
import org.jetbrains.skia.SurfaceColorFormat
|
||||||
import org.jetbrains.skia.SurfaceOrigin
|
import org.jetbrains.skia.SurfaceOrigin
|
||||||
|
import sdl3.SDL_GL_MakeCurrent
|
||||||
import sdl3.SDL_GL_SwapWindow
|
import sdl3.SDL_GL_SwapWindow
|
||||||
|
|
||||||
// ==================
|
// ==================
|
||||||
@@ -27,17 +24,38 @@ import sdl3.SDL_GL_SwapWindow
|
|||||||
Resize: tear down and rebuild the BackendRenderTarget + Surface; the
|
Resize: tear down and rebuild the BackendRenderTarget + Surface; the
|
||||||
DirectContext is reused. The default GL framebuffer (id 0) is wrapped
|
DirectContext is reused. The default GL framebuffer (id 0) is wrapped
|
||||||
directly; we don't manage an offscreen FBO. */
|
directly; we don't manage an offscreen FBO. */
|
||||||
internal class SkiaGLBridge(private val backend: SDL3Backend) : SkiaBridge {
|
internal class SkiaGLBridge(private val backend: Sdl3Backend) : SkiaBridge {
|
||||||
private var fContext: DirectContext? = null
|
private var mContext: DirectContext? = null
|
||||||
private var fRT: BackendRenderTarget? = null
|
private var mRT: BackendRenderTarget? = null
|
||||||
private var fSurface: Surface? = null
|
private var mSurface: Surface? = null
|
||||||
private var fWidth = 0
|
private var mWidth = 0
|
||||||
private var fHeight = 0
|
private var mHeight = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
Binds THIS window's GL context to the calling thread.
|
||||||
|
|
||||||
|
A GL context is per-window but "current" is per-THREAD state, so with more
|
||||||
|
than one window open the last one to touch GL owns the thread. SDL makes a
|
||||||
|
context current once at creation (Sdl3Backend.init) and nothing re-bound it
|
||||||
|
per frame, so window A issued its draw and swap against window B's context:
|
||||||
|
silent corruption while both live, and a hard crash (SIGSEGV in present)
|
||||||
|
the moment B is destroyed and the current context is left dangling.
|
||||||
|
|
||||||
|
Every entry point that touches GL calls this first. Cheap when already
|
||||||
|
current - SDL short-circuits a redundant bind, so the single-window path
|
||||||
|
pays nothing.
|
||||||
|
*/
|
||||||
|
private fun makeCurrent() {
|
||||||
|
val vWindow = backend.window ?: return
|
||||||
|
val vContext = backend.glContext ?: return
|
||||||
|
SDL_GL_MakeCurrent(vWindow.reinterpret(), vContext.reinterpret())
|
||||||
|
}
|
||||||
|
|
||||||
fun init(): Boolean {
|
fun init(): Boolean {
|
||||||
return try {
|
return try {
|
||||||
fContext = DirectContext.makeGL()
|
makeCurrent()
|
||||||
fContext != null
|
mContext = DirectContext.makeGL()
|
||||||
|
mContext != null
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
println("SkiaGLBridge: DirectContext.makeGL failed: ${t.message}")
|
println("SkiaGLBridge: DirectContext.makeGL failed: ${t.message}")
|
||||||
false
|
false
|
||||||
@@ -45,17 +63,20 @@ internal class SkiaGLBridge(private val backend: SDL3Backend) : SkiaBridge {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override val canvas: Canvas
|
override val canvas: Canvas
|
||||||
get() = requireNotNull(fSurface) { "SkiaGLBridge not initialised" }.canvas
|
get() = requireNotNull(mSurface) { "SkiaGLBridge not initialised" }.canvas
|
||||||
|
|
||||||
override fun ensureSize(inWidth: Int, inHeight: Int): Boolean {
|
override fun ensureSize(inWidth: Int, inHeight: Int): Boolean {
|
||||||
if (inWidth == fWidth && inHeight == fHeight && fSurface != null) return true
|
// BEFORE the unchanged-size early return: this runs once per frame and is
|
||||||
|
// what re-binds this window's context for the draw phase that follows.
|
||||||
|
makeCurrent()
|
||||||
|
if (inWidth == mWidth && inHeight == mHeight && mSurface != null) return true
|
||||||
if (inWidth <= 0 || inHeight <= 0) return false
|
if (inWidth <= 0 || inHeight <= 0) return false
|
||||||
val vContext = fContext ?: return false
|
val vContext = mContext ?: return false
|
||||||
|
|
||||||
fSurface?.close()
|
mSurface?.close()
|
||||||
fRT?.close()
|
mRT?.close()
|
||||||
fSurface = null
|
mSurface = null
|
||||||
fRT = null
|
mRT = null
|
||||||
|
|
||||||
// GL_RGBA8 = 0x8058. fbId = 0 binds the default framebuffer.
|
// GL_RGBA8 = 0x8058. fbId = 0 binds the default framebuffer.
|
||||||
// sampleCnt = 0 means no MSAA; stencilBits = 8 is what Skia recommends.
|
// sampleCnt = 0 means no MSAA; stencilBits = 8 is what Skia recommends.
|
||||||
@@ -77,31 +98,35 @@ internal class SkiaGLBridge(private val backend: SDL3Backend) : SkiaBridge {
|
|||||||
println("SkiaGLBridge: Surface.makeFromBackendRenderTarget returned null")
|
println("SkiaGLBridge: Surface.makeFromBackendRenderTarget returned null")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
fRT = vRT
|
mRT = vRT
|
||||||
fSurface = vSurface
|
mSurface = vSurface
|
||||||
fWidth = inWidth
|
mWidth = inWidth
|
||||||
fHeight = inHeight
|
mHeight = inHeight
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun present() {
|
override fun present() {
|
||||||
val vSurface = fSurface ?: return
|
val vSurface = mSurface ?: return
|
||||||
val vWindow = backend.window ?: return
|
val vWindow = backend.window ?: return
|
||||||
|
makeCurrent()
|
||||||
vSurface.flushAndSubmit()
|
vSurface.flushAndSubmit()
|
||||||
SDL_GL_SwapWindow(vWindow.reinterpret())
|
SDL_GL_SwapWindow(vWindow.reinterpret())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun snapshot(): Image? = fSurface?.makeImageSnapshot()
|
override fun snapshot(): Image? = mSurface?.makeImageSnapshot()
|
||||||
|
|
||||||
override fun snapshotBgra(): Triple<Int, Int, ByteArray>? =
|
override fun snapshotBgra(): Triple<Int, Int, ByteArray>? =
|
||||||
readBackBgra(fSurface, fWidth, fHeight)
|
readBackBgra(mSurface, mWidth, mHeight)
|
||||||
|
|
||||||
override fun destroy() {
|
override fun destroy() {
|
||||||
fSurface?.close()
|
// Free this window's GPU objects against its OWN context, not whichever
|
||||||
fRT?.close()
|
// window happens to be current.
|
||||||
fContext?.close()
|
makeCurrent()
|
||||||
fSurface = null
|
mSurface?.close()
|
||||||
fRT = null
|
mRT?.close()
|
||||||
fContext = null
|
mContext?.close()
|
||||||
|
mSurface = null
|
||||||
|
mRT = null
|
||||||
|
mContext = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-21
@@ -25,13 +25,13 @@ import org.jetbrains.skia.Color as SkColor
|
|||||||
elevation shadows ride behind it via skiaLeafDrawer + the SkiaBackedCanvas
|
elevation shadows ride behind it via skiaLeafDrawer + the SkiaBackedCanvas
|
||||||
manual-vendor. */
|
manual-vendor. */
|
||||||
internal class SkiaRenderBackend(
|
internal class SkiaRenderBackend(
|
||||||
private val sdl: SDL3Backend,
|
private val sdl: Sdl3Backend,
|
||||||
private val gpuMode: GpuMode,
|
private val gpuMode: GpuMode,
|
||||||
) : RenderBackend {
|
) : RenderBackend {
|
||||||
|
|
||||||
private val fBridge: SkiaBridge = buildBridge()
|
private val mBridge: SkiaBridge = buildBridge()
|
||||||
private val fSkiaImageCache = SkiaImageCache()
|
private val mSkiaImageCache = SkiaImageCache()
|
||||||
private var fCurrentCanvas: Canvas? = null
|
private var mCurrentCanvas: Canvas? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// skiko's paragraph engine always supports variable-font axes (Material
|
// skiko's paragraph engine always supports variable-font axes (Material
|
||||||
@@ -60,40 +60,40 @@ internal class SkiaRenderBackend(
|
|||||||
// Setting it once in the ctor left it dangling at a CLOSED window's (destroyed)
|
// Setting it once in the ctor left it dangling at a CLOSED window's (destroyed)
|
||||||
// image cache after a multi-window teardown → crash on the surviving window's
|
// image cache after a multi-window teardown → crash on the surviving window's
|
||||||
// next frame, so it is re-pointed at THIS backend before every frame.
|
// next frame, so it is re-pointed at THIS backend before every frame.
|
||||||
private val fLeafDrawer = SkiaLeafDrawer(fSkiaImageCache)
|
private val mLeafDrawer = SkiaLeafDrawer(mSkiaImageCache)
|
||||||
|
|
||||||
override val imageLoader: ImageLoader = object : ImageLoader {
|
override val imageLoader: ImageLoader = object : ImageLoader {
|
||||||
override fun intrinsicSize(inPath: String, inKind: ResourceKind): Size =
|
override fun intrinsicSize(inPath: String, inKind: ResourceKind): Size =
|
||||||
fSkiaImageCache.intrinsicSize(inPath, inKind)
|
mSkiaImageCache.intrinsicSize(inPath, inKind)
|
||||||
override fun readBytes(inPath: String): ByteArray? =
|
override fun readBytes(inPath: String): ByteArray? =
|
||||||
loadComposeResourceBytes(inPath)
|
loadComposeResourceBytes(inPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildBridge(): SkiaBridge = when (gpuMode) {
|
private fun buildBridge(): SkiaBridge = when (gpuMode) {
|
||||||
is GpuMode.Skia.Metal -> makeMetalBridge(sdl) ?: error("Skia.Metal isn't supported on this target")
|
is GpuMode.Metal -> makeMetalBridge(sdl) ?: error("Skia.Metal isn't supported on this target")
|
||||||
is GpuMode.Skia.OpenGL -> SkiaGLBridge(sdl).also { require(it.init()) { "Skia.OpenGL init failed" } }
|
is GpuMode.OpenGL -> SkiaGLBridge(sdl).also { require(it.init()) { "Skia.OpenGL init failed" } }
|
||||||
is GpuMode.Software -> SkiaSurfaceBridge(sdl)
|
is GpuMode.CpuRaster -> SkiaSurfaceBridge(sdl)
|
||||||
is GpuMode.Auto -> error("SkiaRenderBackend received non-Skia gpuMode $gpuMode")
|
is GpuMode.Auto -> error("SkiaRenderBackend received non-Skia gpuMode $gpuMode")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun ensureSize(inPixelWidth: Int, inPixelHeight: Int): Boolean =
|
override fun ensureSize(inPixelWidth: Int, inPixelHeight: Int): Boolean =
|
||||||
fBridge.ensureSize(inPixelWidth, inPixelHeight)
|
mBridge.ensureSize(inPixelWidth, inPixelHeight)
|
||||||
|
|
||||||
override fun beginFrame(inDpr: Float) {
|
override fun beginFrame(inDpr: Float) {
|
||||||
val canvas = fBridge.canvas
|
val canvas = mBridge.canvas
|
||||||
// Clear the surface to Material dark. Without this the Metal back
|
// Clear the surface to Material dark. Without this the Metal back
|
||||||
// buffer shows uninitialized GPU memory (pink). Clearing before scale
|
// buffer shows uninitialized GPU memory (pink). Clearing before scale
|
||||||
// so it covers the whole physical target.
|
// so it covers the whole physical target.
|
||||||
canvas.clear(SkColor.makeARGB(0xFF, 0x12, 0x12, 0x12))
|
canvas.clear(SkColor.makeARGB(0xFF, 0x12, 0x12, 0x12))
|
||||||
canvas.save()
|
canvas.save()
|
||||||
if (inDpr != 1f) canvas.scale(inDpr, inDpr)
|
if (inDpr != 1f) canvas.scale(inDpr, inDpr)
|
||||||
fCurrentCanvas = canvas
|
mCurrentCanvas = canvas
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun drawRoot(inDraw: (androidx.compose.ui.graphics.Canvas) -> Unit) {
|
override fun drawRoot(inDraw: (androidx.compose.ui.graphics.Canvas) -> Unit) {
|
||||||
val canvas = fCurrentCanvas ?: return
|
val canvas = mCurrentCanvas ?: return
|
||||||
// Point the leaf-draw global at THIS window's renderers before drawing.
|
// Point the leaf-draw global at THIS window's renderers before drawing.
|
||||||
skiaLeafDrawer = fLeafDrawer
|
skiaLeafDrawer = mLeafDrawer
|
||||||
// Wrap the live skia canvas as upstream's SkiaBackedCanvas (real gradients/
|
// Wrap the live skia canvas as upstream's SkiaBackedCanvas (real gradients/
|
||||||
// paint/shader). Its port draw contracts forward to skiaLeafDrawer. The
|
// paint/shader). Its port draw contracts forward to skiaLeafDrawer. The
|
||||||
// ImageBitmap-backed offscreen (VectorPainter / DrawCache) now goes through
|
// ImageBitmap-backed offscreen (VectorPainter / DrawCache) now goes through
|
||||||
@@ -104,18 +104,18 @@ internal class SkiaRenderBackend(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun endFrame() {
|
override fun endFrame() {
|
||||||
fCurrentCanvas?.restore()
|
mCurrentCanvas?.restore()
|
||||||
fCurrentCanvas = null
|
mCurrentCanvas = null
|
||||||
fBridge.present()
|
mBridge.present()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun snapshotBgra(): Triple<Int, Int, ByteArray>? = fBridge.snapshotBgra()
|
override fun snapshotBgra(): Triple<Int, Int, ByteArray>? = mBridge.snapshotBgra()
|
||||||
|
|
||||||
override fun destroy() {
|
override fun destroy() {
|
||||||
// Drop the global if it still points at this (about-to-be-destroyed) backend,
|
// Drop the global if it still points at this (about-to-be-destroyed) backend,
|
||||||
// so a stray draw before the next window's drawRoot can't hit freed renderers.
|
// so a stray draw before the next window's drawRoot can't hit freed renderers.
|
||||||
if (skiaLeafDrawer === fLeafDrawer) skiaLeafDrawer = null
|
if (skiaLeafDrawer === mLeafDrawer) skiaLeafDrawer = null
|
||||||
fSkiaImageCache.destroy()
|
mSkiaImageCache.destroy()
|
||||||
fBridge.destroy()
|
mBridge.destroy()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+32
-32
@@ -18,21 +18,21 @@ import sdl3.*
|
|||||||
/** Owns the CPU pixel buffer Skia draws into, plus the streaming SDL_Texture
|
/** Owns the CPU pixel buffer Skia draws into, plus the streaming SDL_Texture
|
||||||
used to push that buffer onto the window. All three (buffer, Skia surface,
|
used to push that buffer onto the window. All three (buffer, Skia surface,
|
||||||
SDL texture) get reallocated when the window resizes. */
|
SDL texture) get reallocated when the window resizes. */
|
||||||
class SkiaSurfaceBridge(private val backend: SDL3Backend) : SkiaBridge {
|
internal class SkiaSurfaceBridge(private val backend: Sdl3Backend) : SkiaBridge {
|
||||||
private var fWidth = 0
|
private var mWidth = 0
|
||||||
private var fHeight = 0
|
private var mHeight = 0
|
||||||
private var fPixels: CPointer<UByteVar>? = null
|
private var mPixels: CPointer<UByteVar>? = null
|
||||||
private var fSurface: Surface? = null
|
private var mSurface: Surface? = null
|
||||||
private var fTexture: COpaquePointer? = null
|
private var mTexture: COpaquePointer? = null
|
||||||
|
|
||||||
override val canvas: Canvas
|
override val canvas: Canvas
|
||||||
get() = requireNotNull(fSurface) { "SkiaSurfaceBridge not initialised" }.canvas
|
get() = requireNotNull(mSurface) { "SkiaSurfaceBridge not initialised" }.canvas
|
||||||
|
|
||||||
val width: Int get() = fWidth
|
val width: Int get() = mWidth
|
||||||
val height: Int get() = fHeight
|
val height: Int get() = mHeight
|
||||||
|
|
||||||
override fun ensureSize(inWidth: Int, inHeight: Int): Boolean {
|
override fun ensureSize(inWidth: Int, inHeight: Int): Boolean {
|
||||||
if (inWidth == fWidth && inHeight == fHeight && fSurface != null) return true
|
if (inWidth == mWidth && inHeight == mHeight && mSurface != null) return true
|
||||||
if (inWidth <= 0 || inHeight <= 0) return false
|
if (inWidth <= 0 || inHeight <= 0) return false
|
||||||
destroy()
|
destroy()
|
||||||
|
|
||||||
@@ -60,34 +60,34 @@ class SkiaSurfaceBridge(private val backend: SDL3Backend) : SkiaBridge {
|
|||||||
}
|
}
|
||||||
SDL_SetTextureBlendMode(vTexture.reinterpret(), SDL_BLENDMODE_NONE)
|
SDL_SetTextureBlendMode(vTexture.reinterpret(), SDL_BLENDMODE_NONE)
|
||||||
|
|
||||||
fPixels = vPixels
|
mPixels = vPixels
|
||||||
fSurface = vSurface
|
mSurface = vSurface
|
||||||
fTexture = vTexture
|
mTexture = vTexture
|
||||||
fWidth = inWidth
|
mWidth = inWidth
|
||||||
fHeight = inHeight
|
mHeight = inHeight
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Skia → SDL_Texture → screen, once per frame. */
|
/** Skia → SDL_Texture → screen, once per frame. */
|
||||||
override fun present() {
|
override fun present() {
|
||||||
val vRenderer = backend.renderer ?: return
|
val vRenderer = backend.renderer ?: return
|
||||||
val vTexture = fTexture ?: return
|
val vTexture = mTexture ?: return
|
||||||
val vSurface = fSurface ?: return
|
val vSurface = mSurface ?: return
|
||||||
val vPixels = fPixels ?: return
|
val vPixels = mPixels ?: return
|
||||||
|
|
||||||
vSurface.flushAndSubmit()
|
vSurface.flushAndSubmit()
|
||||||
SDL_UpdateTexture(vTexture.reinterpret(), null, vPixels, fWidth * 4)
|
SDL_UpdateTexture(vTexture.reinterpret(), null, vPixels, mWidth * 4)
|
||||||
SDL_RenderTexture(vRenderer.reinterpret(), vTexture.reinterpret(), null, null)
|
SDL_RenderTexture(vRenderer.reinterpret(), vTexture.reinterpret(), null, null)
|
||||||
SDL_RenderPresent(vRenderer.reinterpret())
|
SDL_RenderPresent(vRenderer.reinterpret())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun snapshot(): Image? = fSurface?.makeImageSnapshot()
|
override fun snapshot(): Image? = mSurface?.makeImageSnapshot()
|
||||||
|
|
||||||
/** CPU bridge already has the raw pixels in fPixels (RGBA8888 premul).
|
/** CPU bridge already has the raw pixels in mPixels (RGBA8888 premul).
|
||||||
Repack to BGRA so we share one BMP writer with the GPU bridges. */
|
Repack to BGRA so we share one BMP writer with the GPU bridges. */
|
||||||
override fun snapshotBgra(): Triple<Int, Int, ByteArray>? {
|
override fun snapshotBgra(): Triple<Int, Int, ByteArray>? {
|
||||||
val vPixels = fPixels ?: return null
|
val vPixels = mPixels ?: return null
|
||||||
val vCount = fWidth * fHeight
|
val vCount = mWidth * mHeight
|
||||||
val vBytes = ByteArray(vCount * 4)
|
val vBytes = ByteArray(vCount * 4)
|
||||||
for (i in 0 until vCount) {
|
for (i in 0 until vCount) {
|
||||||
val vR = vPixels[i * 4 + 0]
|
val vR = vPixels[i * 4 + 0]
|
||||||
@@ -99,17 +99,17 @@ class SkiaSurfaceBridge(private val backend: SDL3Backend) : SkiaBridge {
|
|||||||
vBytes[i * 4 + 2] = vR.toByte()
|
vBytes[i * 4 + 2] = vR.toByte()
|
||||||
vBytes[i * 4 + 3] = vA.toByte()
|
vBytes[i * 4 + 3] = vA.toByte()
|
||||||
}
|
}
|
||||||
return Triple(fWidth, fHeight, vBytes)
|
return Triple(mWidth, mHeight, vBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun destroy() {
|
override fun destroy() {
|
||||||
fTexture?.let { SDL_DestroyTexture(it.reinterpret()) }
|
mTexture?.let { SDL_DestroyTexture(it.reinterpret()) }
|
||||||
fSurface?.close()
|
mSurface?.close()
|
||||||
fPixels?.let { nativeHeap.free(it) }
|
mPixels?.let { nativeHeap.free(it) }
|
||||||
fTexture = null
|
mTexture = null
|
||||||
fSurface = null
|
mSurface = null
|
||||||
fPixels = null
|
mPixels = null
|
||||||
fWidth = 0
|
mWidth = 0
|
||||||
fHeight = 0
|
mHeight = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -6,7 +6,7 @@ import com.compose.sdl.renderer.skia.SkiaBridge
|
|||||||
// MARK: Windows (mingwX64) GPU defaults
|
// MARK: Windows (mingwX64) GPU defaults
|
||||||
// ==================
|
// ==================
|
||||||
|
|
||||||
// No Metal on Windows. The default Skia bridge is CPU raster (GpuMode.Software →
|
// No Metal on Windows. The default Skia bridge is CPU raster (GpuMode.CpuRaster →
|
||||||
// SkiaSurfaceBridge), so no GPU-context bridge is provided here yet. A D3D/Vulkan
|
// SkiaSurfaceBridge), so no GPU-context bridge is provided here yet. A D3D/Vulkan
|
||||||
// bridge can be added later (Route 1a milestone 2).
|
// bridge can be added later (Route 1a milestone 2).
|
||||||
internal actual fun makeMetalBridge(backend: SDL3Backend): SkiaBridge? = null
|
internal actual fun makeMetalBridge(backend: Sdl3Backend): SkiaBridge? = null
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ internal fun AnimationScreen() {
|
|||||||
"animation.core",
|
"animation.core",
|
||||||
"animate*AsState, AnimationSpec (tween / spring / repeatable / infiniteRepeatable), " +
|
"animate*AsState, AnimationSpec (tween / spring / repeatable / infiniteRepeatable), " +
|
||||||
"Easing (Linear, FastOutSlowIn, CubicBezier), rememberInfiniteTransition. " +
|
"Easing (Linear, FastOutSlowIn, CubicBezier), rememberInfiniteTransition. " +
|
||||||
"All driven by the SDL3FrameClock via withFrameNanos.",
|
"All driven by the Sdl3FrameClock via withFrameNanos.",
|
||||||
)
|
)
|
||||||
|
|
||||||
// ============
|
// ============
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ internal fun GestureScreen() {
|
|||||||
ScreenTitle(
|
ScreenTitle(
|
||||||
"pointerInput + gesture DSL",
|
"pointerInput + gesture DSL",
|
||||||
"Suspending Modifier.pointerInput { } with detectTapGestures / detectDragGestures " +
|
"Suspending Modifier.pointerInput { } with detectTapGestures / detectDragGestures " +
|
||||||
"on top. Awaits resume on the SDL3FrameClock; no per-event polling.",
|
"on top. Awaits resume on the Sdl3FrameClock; no per-event polling.",
|
||||||
)
|
)
|
||||||
|
|
||||||
// ============
|
// ============
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import androidx.compose.foundation.background
|
|||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@@ -10,29 +9,17 @@ import androidx.compose.material3.darkColorScheme
|
|||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.pointer.PointerIcon
|
|
||||||
import androidx.compose.ui.input.pointer.pointerHoverIcon
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.decodeToImageBitmap
|
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
|
||||||
import androidx.compose.ui.layout.onSizeChanged
|
|
||||||
import androidx.compose.ui.layout.positionInRoot
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.compose.sdl.Window
|
import com.compose.sdl.Window
|
||||||
import com.compose.sdl.nativeComposeApp
|
import com.compose.sdl.nativeComposeApp
|
||||||
import com.compose.sdl.nativeComposeWindow
|
|
||||||
import demo.registry.allCategories
|
import demo.registry.allCategories
|
||||||
import demo.shell.App
|
import demo.shell.App
|
||||||
import screens.ExtraWindows
|
import screens.ExtraWindows
|
||||||
import utils.encodeBmpBgra32
|
import utils.encodeBmpBgra32
|
||||||
import utils.parseArgs
|
import utils.parseArgs
|
||||||
import utils.writeFile
|
import utils.writeFile
|
||||||
import kotlinx.cinterop.ExperimentalForeignApi
|
|
||||||
import kotlinx.cinterop.allocArray
|
|
||||||
import kotlinx.cinterop.memScoped
|
|
||||||
import kotlinx.cinterop.toKString
|
|
||||||
|
|
||||||
// ==================
|
// ==================
|
||||||
// MARK: Entry point
|
// MARK: Entry point
|
||||||
@@ -79,6 +66,12 @@ fun main(args: Array<String>) {
|
|||||||
runSearchEscTest()
|
runSearchEscTest()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Verifies the Compose Desktop window attributes (undecorated / resizable /
|
||||||
|
// alwaysOnTop / focusable / enabled) reach SDL, plus the raw SDL escape hatch.
|
||||||
|
if (args.any { it == "--winattrtest" }) {
|
||||||
|
runWindowAttrTest()
|
||||||
|
return
|
||||||
|
}
|
||||||
// Verifies nativeComposeApp multi-window: two Windows render concurrently,
|
// Verifies nativeComposeApp multi-window: two Windows render concurrently,
|
||||||
// one closes via state, the app keeps running on the survivor.
|
// one closes via state, the app keeps running on the survivor.
|
||||||
if (args.any { it == "--multiwintest" }) {
|
if (args.any { it == "--multiwintest" }) {
|
||||||
@@ -288,947 +281,3 @@ private fun com.compose.sdl.ComposeWindowScope.ExtraWindowContent(inId: Int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Boots a window with a BackHandler, injects an Escape key through the live SDL
|
|
||||||
path, and asserts the handler fired - proving ComposeWindow's
|
|
||||||
BackNavigationInput drives the NavigationEventDispatcher (the mechanism that
|
|
||||||
collapses an expanded m3 SearchBar on Escape). */
|
|
||||||
@OptIn(androidx.compose.ui.ExperimentalComposeUiApi::class)
|
|
||||||
private fun runBackTest() {
|
|
||||||
val vBackNoFocus = mutableStateOf(false)
|
|
||||||
val vBackFocused = mutableStateOf(false)
|
|
||||||
val vText = mutableStateOf("")
|
|
||||||
nativeComposeWindow(
|
|
||||||
title = "backtest",
|
|
||||||
width = 400,
|
|
||||||
height = 200,
|
|
||||||
onFrame = { _, frameIndex ->
|
|
||||||
when (frameIndex) {
|
|
||||||
// Phase 1: Escape with NOTHING focused.
|
|
||||||
20 -> {
|
|
||||||
com.compose.sdl.injectKey(41, true) // SDL_SCANCODE_ESCAPE
|
|
||||||
com.compose.sdl.injectKey(41, false)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
// Phase 2: click the text field to focus it, then Escape -
|
|
||||||
// the user-facing SearchBar scenario (field focused while
|
|
||||||
// the back handler should collapse the bar).
|
|
||||||
30 -> { com.compose.sdl.injectMouseEvent(1, 200f, 100f); true }
|
|
||||||
32 -> { com.compose.sdl.injectMouseEvent(2, 200f, 100f); true }
|
|
||||||
44 -> {
|
|
||||||
com.compose.sdl.injectKey(41, true)
|
|
||||||
com.compose.sdl.injectKey(41, false)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
70 -> {
|
|
||||||
println("backtest: noFocus=${vBackNoFocus.value} focused=${vBackFocused.value}")
|
|
||||||
println(
|
|
||||||
if (vBackNoFocus.value && vBackFocused.value)
|
|
||||||
"backtest: PASS (Escape completed back navigation with and without a focused field)"
|
|
||||||
else
|
|
||||||
"backtest: FAIL (noFocus=${vBackNoFocus.value} focusedField=${vBackFocused.value})"
|
|
||||||
)
|
|
||||||
false
|
|
||||||
}
|
|
||||||
else -> true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
androidx.compose.ui.backhandler.BackHandler(enabled = true) {
|
|
||||||
if (!vBackNoFocus.value) vBackNoFocus.value = true else vBackFocused.value = true
|
|
||||||
}
|
|
||||||
MaterialTheme(colorScheme = darkColorScheme()) {
|
|
||||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
|
||||||
// The NEW state-based field - the one m3 SearchBar's InputField
|
|
||||||
// uses - so the probe exercises its key handler, not the legacy
|
|
||||||
// value-based one.
|
|
||||||
androidx.compose.foundation.text.BasicTextField(
|
|
||||||
state = androidx.compose.foundation.text.input.rememberTextFieldState(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Drives a SharedTransitionLayout shared-element morph both directions from
|
|
||||||
the frame counter (no clicks) - it requires the LOOKAHEAD pass, which dies
|
|
||||||
with "LookaheadDelegate has not been measured yet" if the owner drops
|
|
||||||
affectsLookahead measure/relayout requests. Reaching the end frame = PASS. */
|
|
||||||
@OptIn(androidx.compose.animation.ExperimentalSharedTransitionApi::class)
|
|
||||||
private fun runSharedTest() {
|
|
||||||
val vBig = mutableStateOf(false)
|
|
||||||
nativeComposeWindow(
|
|
||||||
title = "sharedtest",
|
|
||||||
width = 300,
|
|
||||||
height = 300,
|
|
||||||
onFrame = { _, vFrame ->
|
|
||||||
when (vFrame) {
|
|
||||||
20 -> { vBig.value = true; true } // small → big morph
|
|
||||||
70 -> { vBig.value = false; true } // big → small morph
|
|
||||||
130 -> {
|
|
||||||
println("sharedtest: PASS (shared-element morph ran both directions)")
|
|
||||||
false
|
|
||||||
}
|
|
||||||
else -> true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
MaterialTheme(colorScheme = darkColorScheme()) {
|
|
||||||
androidx.compose.animation.SharedTransitionLayout {
|
|
||||||
Column {
|
|
||||||
androidx.compose.animation.AnimatedVisibility(visible = !vBig.value) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.sharedElement(
|
|
||||||
sharedContentState = rememberSharedContentState(key = "box"),
|
|
||||||
animatedVisibilityScope = this@AnimatedVisibility,
|
|
||||||
)
|
|
||||||
.size(48.dp)
|
|
||||||
.background(Color(0xFF7C4DFF), RoundedCornerShape(8.dp)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
androidx.compose.animation.AnimatedVisibility(visible = vBig.value) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.sharedElement(
|
|
||||||
sharedContentState = rememberSharedContentState(key = "box"),
|
|
||||||
animatedVisibilityScope = this@AnimatedVisibility,
|
|
||||||
)
|
|
||||||
.size(160.dp)
|
|
||||||
.background(Color(0xFF26A69A), RoundedCornerShape(24.dp)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Boots TWO windows via nativeComposeApp, asserts both render, closes the
|
|
||||||
second by flipping the state that composes its Window(), and asserts the app
|
|
||||||
keeps running on the first - the multi-window lifecycle end-to-end. */
|
|
||||||
private fun runMultiWindowTest() {
|
|
||||||
val vShowSecond = mutableStateOf(true)
|
|
||||||
var vSecondFrames = 0
|
|
||||||
var vResult = "FAIL (main window never reached the end frame)"
|
|
||||||
nativeComposeApp {
|
|
||||||
Window(
|
|
||||||
onCloseRequest = ::exitApplication,
|
|
||||||
title = "multiwin main",
|
|
||||||
width = 400,
|
|
||||||
height = 200,
|
|
||||||
onFrame = { _, vFrame ->
|
|
||||||
when (vFrame) {
|
|
||||||
40 -> {
|
|
||||||
if (vSecondFrames == 0) {
|
|
||||||
vResult = "FAIL (second window never rendered)"
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
vShowSecond.value = false // close the second window via state
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
80 -> {
|
|
||||||
vResult =
|
|
||||||
if (!vShowSecond.value && vSecondFrames > 0)
|
|
||||||
"PASS (both windows rendered; second closed via state; app survived on the first)"
|
|
||||||
else "FAIL (second=$vSecondFrames showSecond=${vShowSecond.value})"
|
|
||||||
false
|
|
||||||
}
|
|
||||||
else -> true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
Text("main window", color = Color.White)
|
|
||||||
}
|
|
||||||
if (vShowSecond.value) {
|
|
||||||
Window(
|
|
||||||
onCloseRequest = { vShowSecond.value = false },
|
|
||||||
title = "multiwin second",
|
|
||||||
width = 300,
|
|
||||||
height = 150,
|
|
||||||
onFrame = { _, _ -> vSecondFrames++; true },
|
|
||||||
) {
|
|
||||||
Text("second window", color = Color.White)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
println("multiwintest: secondFrames=$vSecondFrames")
|
|
||||||
println("multiwintest: $vResult")
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Boots the REAL Search screen, clicks the first SearchBar's input field
|
|
||||||
(expands it), presses Escape, and writes esc_before.bmp / esc_after.bmp -
|
|
||||||
the expanded overlay must be visible in `before` and gone in `after`. */
|
|
||||||
private fun runSearchEscTest() {
|
|
||||||
fun snap(inBridge: com.compose.sdl.RenderBackend, inName: String) {
|
|
||||||
val vSnap = inBridge.snapshotBgra() ?: return
|
|
||||||
val (vW, vH, vBgra) = vSnap
|
|
||||||
writeFile(inName, encodeBmpBgra32(vW, vH, vBgra))
|
|
||||||
println("searchesctest: wrote $inName")
|
|
||||||
}
|
|
||||||
nativeComposeWindow(
|
|
||||||
title = "searchesctest",
|
|
||||||
width = 1000,
|
|
||||||
height = 700,
|
|
||||||
onFrame = { vBridge, vFrame ->
|
|
||||||
when (vFrame) {
|
|
||||||
30 -> { com.compose.sdl.injectMouseEvent(1, 200f, 230f); true }
|
|
||||||
32 -> { com.compose.sdl.injectMouseEvent(2, 200f, 230f); true }
|
|
||||||
70 -> { snap(vBridge, "esc_before.bmp"); true }
|
|
||||||
80 -> {
|
|
||||||
com.compose.sdl.injectKey(41, true) // Escape
|
|
||||||
com.compose.sdl.injectKey(41, false)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
130 -> { snap(vBridge, "esc_after.bmp"); false }
|
|
||||||
else -> true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
MaterialTheme(colorScheme = darkColorScheme()) {
|
|
||||||
val vScroll = rememberScrollState()
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(MaterialTheme.colorScheme.background)
|
|
||||||
.verticalScroll(vScroll)
|
|
||||||
.padding(24.dp),
|
|
||||||
) {
|
|
||||||
screens.M3SearchScreen()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Boots an empty window, then composes screens.Navigation3Screen() at frame 30 -
|
|
||||||
AFTER the window lifecycle reached RESUMED - mirroring the real sidebar flow.
|
|
||||||
Crashes here (e.g. enableSavedStateHandles' INITIALIZED/CREATED contract) never
|
|
||||||
reproduce under --screen, which composes during the initial CREATED composition.
|
|
||||||
PASS = a screenshot gets written and the app exits cleanly. */
|
|
||||||
/** P2.2 soak - cycle through EVERY registered screen kCycles times in ONE process,
|
|
||||||
disposing each via a changing key() so composition + layer (skiko RenderNode)
|
|
||||||
allocate-and-release is exercised repeatedly. After each full cycle, GC then
|
|
||||||
record peak RSS (getrusage ru_maxrss). Peak RSS is monotonic, so after cycle 1 it
|
|
||||||
already reflects visiting every screen once; if there is NO leak it plateaus, if
|
|
||||||
there IS one it keeps climbing each cycle. PASS iff last-cycle peak stays within a
|
|
||||||
ceiling of the first-cycle peak. */
|
|
||||||
@OptIn(kotlin.native.runtime.NativeRuntimeApi::class, ExperimentalForeignApi::class)
|
|
||||||
private fun runSoakTest() {
|
|
||||||
// Disable never-settling animations so RSS reflects composition/layer lifetime,
|
|
||||||
// not live animation-state churn (same seed the screenshot path uses).
|
|
||||||
com.compose.sdl.disableInfiniteAnimations = true
|
|
||||||
com.compose.sdl.useVirtualFrameTime = true
|
|
||||||
val vAll = allCategories().flatMap { it.screens }
|
|
||||||
// CDN_SOAK_SCREEN=<name> repeats ONE screen 40x/cycle (bisect a specific screen);
|
|
||||||
// default cycles through every screen.
|
|
||||||
val vTarget = platform.posix.getenv("CDN_SOAK_SCREEN")?.toKString()
|
|
||||||
val vScreens = if (vTarget != null) { val vS = vAll.first { it.name.equals(vTarget, ignoreCase = true) }; List(40) { vS } }
|
|
||||||
else vAll
|
|
||||||
val vIndex = mutableStateOf(0)
|
|
||||||
// CDN_SOAK_STATIC=1: mount ONE screen once, never remount - measure RSS every 120 frames.
|
|
||||||
// Isolates a per-FRAME leak (RSS climbs with no remounts) from a per-MOUNT leak.
|
|
||||||
val vStatic = platform.posix.getenv("CDN_SOAK_STATIC")?.toKString() == "1"
|
|
||||||
val kFramesPerScreen = if (vStatic) 120 else 3
|
|
||||||
val kCycles = platform.posix.getenv("CDN_SOAK_CYCLES")?.toKString()?.toIntOrNull() ?: 3
|
|
||||||
val vRssPerCycleMb = mutableListOf<Long>()
|
|
||||||
var vShown = 0
|
|
||||||
|
|
||||||
nativeComposeWindow(
|
|
||||||
title = "soaktest",
|
|
||||||
width = 1000,
|
|
||||||
height = 700,
|
|
||||||
onFrame = { _, vFrame ->
|
|
||||||
if (vFrame > 0 && vFrame % kFramesPerScreen == 0) {
|
|
||||||
if (vStatic) {
|
|
||||||
// No remount - just pump frames on the one mounted screen.
|
|
||||||
kotlin.native.runtime.GC.collect()
|
|
||||||
vRssPerCycleMb.add(currentResidentMb())
|
|
||||||
println("soaktest[static]: measure ${vRssPerCycleMb.size}: currentRSS=${vRssPerCycleMb.last()}MB")
|
|
||||||
} else {
|
|
||||||
vShown++
|
|
||||||
vIndex.value = vShown % vScreens.size
|
|
||||||
if (vShown % vScreens.size == 0) {
|
|
||||||
kotlin.native.runtime.GC.collect()
|
|
||||||
vRssPerCycleMb.add(currentResidentMb())
|
|
||||||
println("soaktest: cycle ${vRssPerCycleMb.size}: currentRSS=${vRssPerCycleMb.last()}MB")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (vRssPerCycleMb.size >= kCycles) {
|
|
||||||
val vFirst = vRssPerCycleMb.first()
|
|
||||||
val vLast = vRssPerCycleMb.last()
|
|
||||||
val vGrowth = vLast - vFirst
|
|
||||||
val vCeiling = maxOf(48L, vFirst / 4) // 25% or 48MB, whichever larger
|
|
||||||
println("soaktest: currentRSS/cycle(MB)=$vRssPerCycleMb (${vScreens.size} screens x $kCycles cycles)")
|
|
||||||
if (vGrowth <= vCeiling) {
|
|
||||||
println("soaktest: PASS (current RSS grew ${vGrowth}MB over cycles 1->$kCycles, within ${vCeiling}MB ceiling)")
|
|
||||||
} else {
|
|
||||||
println("soaktest: FAIL (current RSS grew ${vGrowth}MB > ${vCeiling}MB ceiling - possible leak)")
|
|
||||||
}
|
|
||||||
false
|
|
||||||
} else true
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
MaterialTheme(colorScheme = darkColorScheme()) {
|
|
||||||
val vScroll = rememberScrollState()
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(MaterialTheme.colorScheme.background)
|
|
||||||
.verticalScroll(vScroll)
|
|
||||||
.padding(24.dp),
|
|
||||||
) {
|
|
||||||
// key(vShown) forces a FULL dispose+recompose each advance (even when the
|
|
||||||
// target screen repeats) - the allocate/release churn we soak. Reading
|
|
||||||
// vIndex.value (a State) is what triggers the recomposition each mount.
|
|
||||||
key(vShown) {
|
|
||||||
vScreens[vIndex.value].content()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** CURRENT resident set (MB): current RSS can DROP after GC, so it distinguishes a
|
|
||||||
true leak (ratchets up) from K/N allocator high-water, unlike getrusage's
|
|
||||||
monotonic peak. posix reads it from `ps`; mingw has no `ps`/popen and returns
|
|
||||||
-1 (the soak gate runs on macOS/Linux - see scripts/verify-mac.sh). */
|
|
||||||
internal expect fun currentResidentMb(): Long
|
|
||||||
|
|
||||||
/** --localetest: prints Locale.current / LocaleList.current (should reflect the OS
|
|
||||||
preferred locales via SDL) and screenshots an M3 DatePicker, whose headline and
|
|
||||||
navigation labels are M3-translated by Locale.current. Run under a forced locale,
|
|
||||||
e.g. `demo.kexe --localetest -AppleLanguages "(fr-FR)"` on macOS. */
|
|
||||||
@OptIn(androidx.compose.material3.ExperimentalMaterial3Api::class)
|
|
||||||
private fun runLocaleTest() {
|
|
||||||
nativeComposeWindow(
|
|
||||||
title = "localetest",
|
|
||||||
width = 420,
|
|
||||||
height = 560,
|
|
||||||
onFrame = { vBridge, vFrame ->
|
|
||||||
when (vFrame) {
|
|
||||||
2 -> {
|
|
||||||
val vCur = androidx.compose.ui.text.intl.Locale.current
|
|
||||||
val vList = androidx.compose.ui.text.intl.LocaleList.current
|
|
||||||
println("localetest: Locale.current=${vCur.toLanguageTag()}")
|
|
||||||
println("localetest: LocaleList.current=[${vList.localeList.joinToString { it.toLanguageTag() }}]")
|
|
||||||
true
|
|
||||||
}
|
|
||||||
40 -> {
|
|
||||||
val vSnap = vBridge.snapshotBgra()
|
|
||||||
if (vSnap != null) {
|
|
||||||
val (vW, vH, vBgra) = vSnap
|
|
||||||
writeFile("localetest.bmp", encodeBmpBgra32(vW, vH, vBgra))
|
|
||||||
println("localetest: wrote localetest.bmp (${vW}x${vH})")
|
|
||||||
} else println("localetest: FAIL (no snapshot)")
|
|
||||||
false
|
|
||||||
}
|
|
||||||
else -> true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
MaterialTheme(colorScheme = darkColorScheme()) {
|
|
||||||
androidx.compose.material3.Surface(modifier = Modifier.fillMaxSize()) {
|
|
||||||
val vState = androidx.compose.material3.rememberDatePickerState()
|
|
||||||
androidx.compose.material3.DatePicker(state = vState)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** --cursortest: injects hover moves over pointerHoverIcon(Text) / (Hand) regions
|
|
||||||
and a bare background, printing the applied SDL system cursor after each. Proves
|
|
||||||
PointerIcon -> SDL_SetCursor end-to-end through the live hover pipeline. */
|
|
||||||
private fun runCursorTest() {
|
|
||||||
var vBg = "?"; var vText = "?"; var vHand = "?"
|
|
||||||
nativeComposeWindow(
|
|
||||||
title = "cursortest",
|
|
||||||
width = 400,
|
|
||||||
height = 400,
|
|
||||||
onFrame = { _, vFrame ->
|
|
||||||
when (vFrame) {
|
|
||||||
10 -> { com.compose.sdl.injectMouseEvent(0, 200f, 300f); true } // background (below both boxes)
|
|
||||||
14 -> { vBg = com.compose.sdl.appliedCursorName(); true }
|
|
||||||
20 -> { com.compose.sdl.injectMouseEvent(0, 200f, 50f); true } // Text box
|
|
||||||
24 -> { vText = com.compose.sdl.appliedCursorName(); true }
|
|
||||||
30 -> { com.compose.sdl.injectMouseEvent(0, 200f, 150f); true } // Hand box
|
|
||||||
34 -> {
|
|
||||||
vHand = com.compose.sdl.appliedCursorName()
|
|
||||||
println("cursortest: background=$vBg text=$vText hand=$vHand")
|
|
||||||
val vOk = vBg.endsWith("DEFAULT") && vText.endsWith("TEXT") && vHand.endsWith("POINTER")
|
|
||||||
println(if (vOk) "cursortest: PASS" else "cursortest: FAIL")
|
|
||||||
false
|
|
||||||
}
|
|
||||||
else -> true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
MaterialTheme(colorScheme = darkColorScheme()) {
|
|
||||||
Column(Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background)) {
|
|
||||||
Box(Modifier.fillMaxWidth().height(100.dp).pointerHoverIcon(PointerIcon.Text))
|
|
||||||
Box(Modifier.fillMaxWidth().height(100.dp).pointerHoverIcon(PointerIcon.Hand))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** --windowinfotest: prints LocalWindowInfo.isWindowFocused / containerSize /
|
|
||||||
containerDpSize. containerSize starts Zero and becomes the window pixel size
|
|
||||||
after the first measure - proof it's fed from the live root constraints. */
|
|
||||||
private fun runWindowInfoTest() {
|
|
||||||
nativeComposeWindow(
|
|
||||||
title = "windowinfotest",
|
|
||||||
width = 400,
|
|
||||||
height = 300,
|
|
||||||
onFrame = { _, vFrame -> vFrame < 6 },
|
|
||||||
) {
|
|
||||||
val vWi = androidx.compose.ui.platform.LocalWindowInfo.current
|
|
||||||
androidx.compose.runtime.LaunchedEffect(vWi.containerSize, vWi.isWindowFocused) {
|
|
||||||
println("windowinfotest: focused=${vWi.isWindowFocused} containerSize=${vWi.containerSize} containerDpSize=${vWi.containerDpSize}")
|
|
||||||
}
|
|
||||||
MaterialTheme(colorScheme = darkColorScheme()) {
|
|
||||||
Box(Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** --imetest: focuses a BasicTextField, injects an IME composition (SDL TEXT_EDITING
|
|
||||||
"ni"), then a commit (SDL TEXT_INPUT "に"). Verifies the preedit shows a
|
|
||||||
composing region and the commit REPLACES it (not appends) - the real IME path. */
|
|
||||||
private fun runImeTest() {
|
|
||||||
val vField = mutableStateOf(androidx.compose.ui.text.input.TextFieldValue(""))
|
|
||||||
nativeComposeWindow(
|
|
||||||
title = "imetest",
|
|
||||||
width = 500,
|
|
||||||
height = 200,
|
|
||||||
onFrame = { _, vFrame ->
|
|
||||||
when (vFrame) {
|
|
||||||
12 -> { com.compose.sdl.injectMouseEvent(1, 250f, 100f); true } // focus centered field
|
|
||||||
14 -> { com.compose.sdl.injectMouseEvent(2, 250f, 100f); true }
|
|
||||||
34 -> { com.compose.sdl.injectTextEditing("ni"); true } // composing (preedit)
|
|
||||||
40 -> { println("imetest: composing text='${vField.value.text}' composition=${vField.value.composition}"); true }
|
|
||||||
50 -> { com.compose.sdl.injectTextInput("に"); true } // commit "に"
|
|
||||||
60 -> {
|
|
||||||
println("imetest: committed text='${vField.value.text}' composition=${vField.value.composition}")
|
|
||||||
val vOk = vField.value.text == "に" && vField.value.composition == null
|
|
||||||
println(if (vOk) "imetest: PASS" else "imetest: FAIL")
|
|
||||||
false
|
|
||||||
}
|
|
||||||
else -> true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
MaterialTheme(colorScheme = darkColorScheme()) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background),
|
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
) {
|
|
||||||
androidx.compose.foundation.text.BasicTextField(
|
|
||||||
value = vField.value,
|
|
||||||
onValueChange = { vField.value = it },
|
|
||||||
textStyle = androidx.compose.ui.text.TextStyle(color = Color.White, fontSize = 24.sp),
|
|
||||||
cursorBrush = androidx.compose.ui.graphics.SolidColor(Color.White),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** --imagebytestest: decodes an in-memory 2x2 BMP via ByteArray.decodeToImageBitmap()
|
|
||||||
(routes to createImageBitmap(bytes)) through Skia's image decoder. Needs a window
|
|
||||||
so the render backend installs the encoded-image decoder. */
|
|
||||||
private fun runImageBytesTest() {
|
|
||||||
nativeComposeWindow(
|
|
||||||
title = "imagebytestest",
|
|
||||||
width = 200,
|
|
||||||
height = 200,
|
|
||||||
onFrame = { _, vFrame ->
|
|
||||||
if (vFrame >= 2) {
|
|
||||||
try {
|
|
||||||
val vBitmap = tinyRedBmp().decodeToImageBitmap()
|
|
||||||
println("imagebytestest: decoded ${vBitmap.width}x${vBitmap.height}")
|
|
||||||
println(if (vBitmap.width == 2 && vBitmap.height == 2) "imagebytestest: PASS" else "imagebytestest: FAIL (wrong size)")
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
println("imagebytestest: FAIL (${e::class.simpleName}: ${e.message})")
|
|
||||||
}
|
|
||||||
false
|
|
||||||
} else true
|
|
||||||
},
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** --fonttest: same sample in FontFamily.Default (sans) and FontFamily.Monospace.
|
|
||||||
Monospace used to collapse to the default sans; now it renders NotoSansMono
|
|
||||||
(bundled because this source references FontFamily.Monospace). */
|
|
||||||
private fun runFontTest() {
|
|
||||||
nativeComposeWindow(
|
|
||||||
title = "fonttest",
|
|
||||||
width = 560,
|
|
||||||
height = 220,
|
|
||||||
onFrame = { vBridge, vFrame ->
|
|
||||||
if (vFrame >= 10) {
|
|
||||||
val vSnap = vBridge.snapshotBgra()
|
|
||||||
if (vSnap != null) {
|
|
||||||
val (vW, vH, vBgra) = vSnap
|
|
||||||
writeFile("fonttest.bmp", encodeBmpBgra32(vW, vH, vBgra))
|
|
||||||
println("fonttest: wrote fonttest.bmp (${vW}x${vH})")
|
|
||||||
}
|
|
||||||
false
|
|
||||||
} else true
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
MaterialTheme(colorScheme = darkColorScheme()) {
|
|
||||||
Column(Modifier.fillMaxSize().background(Color(0xFF202020)).padding(20.dp)) {
|
|
||||||
val vSample = "Illegal1 lIO0 {}=>"
|
|
||||||
Text(vSample, color = Color.White, fontSize = 30.sp)
|
|
||||||
Text(
|
|
||||||
vSample, color = Color.Cyan, fontSize = 30.sp,
|
|
||||||
fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// A minimal 2x2 24bpp red BMP (no compression) - valid decoder input.
|
|
||||||
private fun tinyRedBmp(): ByteArray {
|
|
||||||
fun le16(v: Int) = byteArrayOf((v and 0xFF).toByte(), ((v shr 8) and 0xFF).toByte())
|
|
||||||
fun le32(v: Int) = byteArrayOf(
|
|
||||||
(v and 0xFF).toByte(), ((v shr 8) and 0xFF).toByte(),
|
|
||||||
((v shr 16) and 0xFF).toByte(), ((v shr 24) and 0xFF).toByte(),
|
|
||||||
)
|
|
||||||
val vPixel = byteArrayOf(0, 0, 0xFF.toByte()) // BGR red
|
|
||||||
val vRow = vPixel + vPixel + byteArrayOf(0, 0) // 2px + 2 pad = 8 bytes (4-byte aligned)
|
|
||||||
val vPixels = vRow + vRow // 2 rows = 16 bytes
|
|
||||||
val vFileHeader = byteArrayOf('B'.code.toByte(), 'M'.code.toByte()) + le32(70) + le32(0) + le32(54)
|
|
||||||
val vDib = le32(40) + le32(2) + le32(2) + le16(1) + le16(24) + le32(0) + le32(16) +
|
|
||||||
le32(2835) + le32(2835) + le32(0) + le32(0)
|
|
||||||
return vFileHeader + vDib + vPixels
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun runNav3Test() {
|
|
||||||
val vShow = mutableStateOf(false)
|
|
||||||
nativeComposeWindow(
|
|
||||||
title = "nav3test",
|
|
||||||
width = 1000,
|
|
||||||
height = 700,
|
|
||||||
onFrame = { vBridge, vFrame ->
|
|
||||||
when (vFrame) {
|
|
||||||
30 -> { vShow.value = true; true }
|
|
||||||
// Push Detail #1 (click its card) → per-entry lifecycle goes
|
|
||||||
// CREATED/STARTED during the slide, RESUMED once settled.
|
|
||||||
60 -> { com.compose.sdl.injectMouseEvent(1, 500f, 216f); true }
|
|
||||||
62 -> { com.compose.sdl.injectMouseEvent(2, 500f, 216f); true }
|
|
||||||
// Pop with ESC → ON_PAUSE, then CREATED while animating out,
|
|
||||||
// then the entry disposes.
|
|
||||||
120 -> {
|
|
||||||
com.compose.sdl.injectKey(41, true)
|
|
||||||
com.compose.sdl.injectKey(41, false)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
180 -> {
|
|
||||||
val vSnap = vBridge.snapshotBgra()
|
|
||||||
if (vSnap != null) {
|
|
||||||
val (vW, vH, vBgra) = vSnap
|
|
||||||
writeFile("nav3late.bmp", encodeBmpBgra32(vW, vH, vBgra))
|
|
||||||
println("nav3test: wrote nav3late.bmp (${vW}x${vH})")
|
|
||||||
println("nav3test: PASS (Navigation3 composed at RESUMED; push + pop ran)")
|
|
||||||
} else println("nav3test: FAIL (no snapshot)")
|
|
||||||
false
|
|
||||||
}
|
|
||||||
else -> true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
MaterialTheme(colorScheme = darkColorScheme()) {
|
|
||||||
Box(modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background)) {
|
|
||||||
if (vShow.value) {
|
|
||||||
Box(modifier = Modifier.padding(24.dp)) { screens.Navigation3Screen() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Frame-by-frame trace of AnimatedVisibility(fadeIn+expandVertically / fadeOut+
|
|
||||||
shrinkVertically): logs the AV container's animated size and the window-Y of a
|
|
||||||
marker Box below it every frame, toggling visibility three times. Diagnoses
|
|
||||||
(a) a size snap between just-before-end and end of the animation and (b)
|
|
||||||
instant (non-animated) transitions on subsequent toggles.
|
|
||||||
|
|
||||||
Expected healthy trace: ~24 smooth frames per toggle ending exactly at 0/60,
|
|
||||||
PLUS one 16px marker jump when the fully-shrunk node unmounts (exit end) or
|
|
||||||
mounts (enter start) - that's the parent's spacedBy(16) collapsing, inherent
|
|
||||||
upstream behaviour (spacing applies to zero-height children too), NOT a bug. */
|
|
||||||
private fun runAnimVisTest() {
|
|
||||||
val vShown = mutableStateOf(true)
|
|
||||||
var vAvSize = androidx.compose.ui.unit.IntSize(-1, -1)
|
|
||||||
var vMarkerY = -1f
|
|
||||||
var vLastLog = ""
|
|
||||||
nativeComposeWindow(
|
|
||||||
title = "animvistest",
|
|
||||||
width = 600,
|
|
||||||
height = 500,
|
|
||||||
onFrame = { _, vFrame ->
|
|
||||||
// Log only when something moved - keeps the trace readable.
|
|
||||||
val vLine = "size=$vAvSize markerY=$vMarkerY shown=${vShown.value}"
|
|
||||||
if (vLine != vLastLog) {
|
|
||||||
println("animvistest: f=$vFrame $vLine")
|
|
||||||
vLastLog = vLine
|
|
||||||
}
|
|
||||||
when (vFrame) {
|
|
||||||
60 -> { println("animvistest: === HIDE 1 ==="); vShown.value = false; true }
|
|
||||||
140 -> { println("animvistest: === SHOW 2 ==="); vShown.value = true; true }
|
|
||||||
220 -> { println("animvistest: === HIDE 3 ==="); vShown.value = false; true }
|
|
||||||
300 -> false
|
|
||||||
else -> true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
MaterialTheme(colorScheme = darkColorScheme()) {
|
|
||||||
// spacedBy mirrors the demo's Section column - the end-of-exit jump
|
|
||||||
// reported on the FoundationExtra screen involves the spacing around
|
|
||||||
// the AnimatedVisibility node collapsing when the node unmounts.
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
|
||||||
) {
|
|
||||||
Box(Modifier.size(30.dp).background(androidx.compose.ui.graphics.Color(0xFFC07040)))
|
|
||||||
androidx.compose.animation.AnimatedVisibility(
|
|
||||||
visible = vShown.value,
|
|
||||||
modifier = Modifier.onSizeChanged { vAvSize = it },
|
|
||||||
enter = androidx.compose.animation.fadeIn() + androidx.compose.animation.expandVertically(),
|
|
||||||
exit = androidx.compose.animation.fadeOut() + androidx.compose.animation.shrinkVertically(),
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(60.dp)
|
|
||||||
.background(androidx.compose.ui.graphics.Color(0xFF7040C0)),
|
|
||||||
) {
|
|
||||||
Text("Animated content", modifier = Modifier.padding(14.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(20.dp)
|
|
||||||
.background(androidx.compose.ui.graphics.Color(0xFF40C070))
|
|
||||||
.onGloballyPositioned { vMarkerY = it.positionInRoot().y },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Boots a window whose full-size surface opens an m3 AlertDialog on click (the real
|
|
||||||
material3 → ui.window.Dialog path, default DialogProperties ⇒ animateTransition on),
|
|
||||||
injects the click, then dumps screenshots during the 0.2s appearance animation and
|
|
||||||
after it settles; then injects Escape and dumps mid-disappearance (0.1s reverse,
|
|
||||||
via the popup host's exit deferral) + after. Visual check: mid shots must show the
|
|
||||||
dialog semi-transparent, slightly scaled-down and shifted down vs the settled shot,
|
|
||||||
and exit_end must show no dialog at all - skiko-parity animation both ways. */
|
|
||||||
private fun runDialogAnimTest() {
|
|
||||||
fun snap(inBridge: com.compose.sdl.RenderBackend, inName: String) {
|
|
||||||
val vSnap = inBridge.snapshotBgra() ?: return
|
|
||||||
val (vW, vH, vBgra) = vSnap
|
|
||||||
writeFile(inName, encodeBmpBgra32(vW, vH, vBgra))
|
|
||||||
println("dialoganimtest: wrote $inName")
|
|
||||||
}
|
|
||||||
nativeComposeWindow(
|
|
||||||
title = "dialoganimtest",
|
|
||||||
width = 1000,
|
|
||||||
height = 700,
|
|
||||||
onFrame = { vBridge, vFrame ->
|
|
||||||
when (vFrame) {
|
|
||||||
30 -> { com.compose.sdl.injectMouseEvent(1, 500f, 350f); true }
|
|
||||||
32 -> { com.compose.sdl.injectMouseEvent(2, 500f, 350f); true }
|
|
||||||
36 -> { snap(vBridge, "dialog_mid1.bmp"); true }
|
|
||||||
40 -> { snap(vBridge, "dialog_mid2.bmp"); true }
|
|
||||||
90 -> { snap(vBridge, "dialog_end.bmp"); true }
|
|
||||||
92 -> {
|
|
||||||
com.compose.sdl.injectKey(41, true) // Escape → dismiss
|
|
||||||
com.compose.sdl.injectKey(41, false)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
97 -> { snap(vBridge, "dialog_exit_mid.bmp"); true }
|
|
||||||
140 -> { snap(vBridge, "dialog_exit_end.bmp"); false }
|
|
||||||
else -> true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
MaterialTheme(colorScheme = darkColorScheme()) {
|
|
||||||
var vShow by remember { mutableStateOf(false) }
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(MaterialTheme.colorScheme.background)
|
|
||||||
.clickable { vShow = true },
|
|
||||||
)
|
|
||||||
if (vShow) {
|
|
||||||
androidx.compose.material3.AlertDialog(
|
|
||||||
onDismissRequest = { vShow = false },
|
|
||||||
title = { Text("Animated dialog") },
|
|
||||||
text = { Text("Appearance animation parity with the JVM (skiko) Dialog.") },
|
|
||||||
confirmButton = {
|
|
||||||
androidx.compose.material3.TextButton(onClick = { vShow = false }) { Text("OK") }
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Boots a real window, then builds an upstream Paragraph via the
|
|
||||||
vendored factory (→ SkiaParagraph) for a long string constrained to a narrow width. Verifies it
|
|
||||||
wrapped to multiple lines, has positive size, and that getHorizontalPosition/getOffsetForPosition
|
|
||||||
round-trip - proving the paragraph-engine measurement bridge works. */
|
|
||||||
private fun runParagraphTest() {
|
|
||||||
nativeComposeWindow(
|
|
||||||
title = "paragraphtest",
|
|
||||||
width = 400,
|
|
||||||
height = 200,
|
|
||||||
onFrame = { _, frameIndex ->
|
|
||||||
if (frameIndex == 10) {
|
|
||||||
val vP = androidx.compose.ui.text.Paragraph(
|
|
||||||
text = "Hello world foo bar baz qux quux corge grault garply waldo",
|
|
||||||
style = androidx.compose.ui.text.TextStyle(fontSize = 16.sp),
|
|
||||||
constraints = androidx.compose.ui.unit.Constraints(maxWidth = 120),
|
|
||||||
density = androidx.compose.ui.unit.Density(1f),
|
|
||||||
fontFamilyResolver = androidx.compose.ui.text.font.createFontFamilyResolver(),
|
|
||||||
overflow = androidx.compose.ui.text.style.TextOverflow.Clip,
|
|
||||||
)
|
|
||||||
val vHpos = vP.getHorizontalPosition(3, true)
|
|
||||||
val vOffBack = vP.getOffsetForPosition(androidx.compose.ui.geometry.Offset(vHpos, 2f))
|
|
||||||
val vLine = vP.getLineForOffset(30)
|
|
||||||
println("paragraphtest: lineCount=${vP.lineCount} w=${vP.width} h=${vP.height} hpos(3)=$vHpos offBack=$vOffBack lineFor(30)=$vLine")
|
|
||||||
val vPass = vP.lineCount >= 2 && vP.height > 0f && vP.width > 0f && vOffBack in 2..4
|
|
||||||
println(if (vPass) "paragraphtest: PASS (real width-wrapped Paragraph via SkiaParagraph)" else "paragraphtest: FAIL")
|
|
||||||
false
|
|
||||||
} else true
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
Box(modifier = Modifier.fillMaxSize()) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Prints paragraph metrics (single-line cell, lineHeight-styled single + triple line,
|
|
||||||
first baseline) for a font-size sweep at density 1 - the native half of the
|
|
||||||
metrics-alignment probe. Mirror of MainJvm's `--metrics`; both must print the same
|
|
||||||
numbers for the parity text drift to vanish. */
|
|
||||||
private fun runMetricsProbe() {
|
|
||||||
fun paragraph(inText: String, inSize: Int, inLineHeight: Int?, inM3Style: Boolean): androidx.compose.ui.text.Paragraph =
|
|
||||||
androidx.compose.ui.text.Paragraph(
|
|
||||||
text = inText,
|
|
||||||
style = androidx.compose.ui.text.TextStyle(
|
|
||||||
fontSize = inSize.sp,
|
|
||||||
lineHeight = inLineHeight?.sp ?: androidx.compose.ui.unit.TextUnit.Unspecified,
|
|
||||||
lineHeightStyle = if (inM3Style) {
|
|
||||||
androidx.compose.ui.text.style.LineHeightStyle(
|
|
||||||
alignment = androidx.compose.ui.text.style.LineHeightStyle.Alignment.Center,
|
|
||||||
trim = androidx.compose.ui.text.style.LineHeightStyle.Trim.None,
|
|
||||||
)
|
|
||||||
} else null,
|
|
||||||
),
|
|
||||||
constraints = androidx.compose.ui.unit.Constraints(maxWidth = 10_000),
|
|
||||||
density = androidx.compose.ui.unit.Density(1f),
|
|
||||||
fontFamilyResolver = androidx.compose.ui.text.font.createFontFamilyResolver(),
|
|
||||||
overflow = androidx.compose.ui.text.style.TextOverflow.Clip,
|
|
||||||
)
|
|
||||||
nativeComposeWindow(
|
|
||||||
title = "metricsprobe",
|
|
||||||
width = 300,
|
|
||||||
height = 200,
|
|
||||||
onFrame = { _, frameIndex ->
|
|
||||||
if (frameIndex == 10) {
|
|
||||||
for (vSize in listOf(11, 12, 14, 16, 22, 24)) {
|
|
||||||
val vLh = vSize + 6
|
|
||||||
val vCell = paragraph("Hg", vSize, null, false)
|
|
||||||
val vOne = paragraph("Hg", vSize, vLh, false)
|
|
||||||
val vThree = paragraph("Hg\nHg\nHg", vSize, vLh, false)
|
|
||||||
val vOneM3 = paragraph("Hg", vSize, vLh, true)
|
|
||||||
val vThreeM3 = paragraph("Hg\nHg\nHg", vSize, vLh, true)
|
|
||||||
println(
|
|
||||||
"metrics: size=$vSize lh=$vLh cell=${vCell.height} " +
|
|
||||||
"one=${vOne.height} three=${vThree.height} " +
|
|
||||||
"base1=${vOne.firstBaseline} base3=${vThree.lastBaseline} " +
|
|
||||||
"oneM3=${vOneM3.height} threeM3=${vThreeM3.height} base1M3=${vOneM3.firstBaseline}"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
for ((vS, vL) in listOf(24 to 24, 24 to 25, 32 to 24, 16 to 16)) {
|
|
||||||
val vB = paragraph("Hg", vS, vL, true)
|
|
||||||
println("metrics: boundary $vS/$vL m3=${vB.height} base=${vB.firstBaseline}")
|
|
||||||
}
|
|
||||||
val vBig = paragraph("42", 48, 24, false)
|
|
||||||
val vBigM3 = paragraph("42", 48, 24, true)
|
|
||||||
println("metrics: big48/lh24 raw=${vBig.height} base=${vBig.firstBaseline} m3=${vBigM3.height} baseM3=${vBigM3.firstBaseline}")
|
|
||||||
println("metricsprobe: DONE")
|
|
||||||
false
|
|
||||||
} else true
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
Box(modifier = Modifier.fillMaxSize()) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================
|
|
||||||
// MARK: clicktest
|
|
||||||
// ==================
|
|
||||||
|
|
||||||
/** Boots a real nativeComposeWindow with a full-size clickable box, then injects
|
|
||||||
move→press→release SDL mouse events a few frames apart (giving the upstream
|
|
||||||
clickable's gesture coroutine time to launch + await between frames), and prints
|
|
||||||
PASS/FAIL based on whether onClick fired. Proves the whole vendored interaction
|
|
||||||
pipeline works under the real Sdl3MainDispatcher + frame loop. */
|
|
||||||
private fun runClickTest() {
|
|
||||||
var vClicks = 0
|
|
||||||
nativeComposeWindow(
|
|
||||||
title = "clicktest",
|
|
||||||
width = 400,
|
|
||||||
height = 300,
|
|
||||||
onFrame = { _, frameIndex ->
|
|
||||||
when (frameIndex) {
|
|
||||||
20 -> { com.compose.sdl.injectMouseEvent(0, 200f, 150f); true }
|
|
||||||
26 -> { com.compose.sdl.injectMouseEvent(1, 200f, 150f); true }
|
|
||||||
32 -> { com.compose.sdl.injectMouseEvent(2, 200f, 150f); true }
|
|
||||||
70 -> {
|
|
||||||
println(
|
|
||||||
if (vClicks > 0) "clicktest: PASS ($vClicks click(s) via upstream clickable)"
|
|
||||||
else "clicktest: FAIL (0 clicks - upstream clickable did not fire)"
|
|
||||||
)
|
|
||||||
false
|
|
||||||
}
|
|
||||||
else -> true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
MaterialTheme(colorScheme = darkColorScheme()) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(Color(0xFF303040))
|
|
||||||
.clickable { vClicks++; println("clicktest: onClick fired -> $vClicks") },
|
|
||||||
) {
|
|
||||||
Text("Click test", color = Color.White, fontSize = 16.sp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Same injection harness as clicktest, but the target is a Material Switch (Modifier.toggleable
|
|
||||||
from the vendored foundation.selection). Asserts onCheckedChange flipped the state - proving
|
|
||||||
toggleable rides the same verified upstream interaction path as clickable. */
|
|
||||||
private fun runToggleTest() {
|
|
||||||
var vChecked = false
|
|
||||||
var vChanges = 0
|
|
||||||
nativeComposeWindow(
|
|
||||||
title = "toggletest",
|
|
||||||
width = 400,
|
|
||||||
height = 300,
|
|
||||||
onFrame = { _, frameIndex ->
|
|
||||||
when (frameIndex) {
|
|
||||||
20 -> { com.compose.sdl.injectMouseEvent(0, 200f, 150f); true }
|
|
||||||
26 -> { com.compose.sdl.injectMouseEvent(1, 200f, 150f); true }
|
|
||||||
32 -> { com.compose.sdl.injectMouseEvent(2, 200f, 150f); true }
|
|
||||||
70 -> {
|
|
||||||
println(
|
|
||||||
if (vChanges > 0 && vChecked) "toggletest: PASS (switch toggled to $vChecked via toggleable)"
|
|
||||||
else "toggletest: FAIL (changes=$vChanges checked=$vChecked)"
|
|
||||||
)
|
|
||||||
false
|
|
||||||
}
|
|
||||||
else -> true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
MaterialTheme(colorScheme = darkColorScheme()) {
|
|
||||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
|
||||||
androidx.compose.material3.Switch(
|
|
||||||
checked = vChecked,
|
|
||||||
onCheckedChange = { vChecked = it; vChanges++; println("toggletest: onCheckedChange -> $it") },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Boots a window with a real BasicTextField, clicks it to focus (focus-on-click via the
|
|
||||||
FocusOwner), then injects TEXT_INPUT ("A","B") and a Backspace key through the live SDL path.
|
|
||||||
Asserts the field edits to "A" - proving click-to-focus + typing + editing keys route to the
|
|
||||||
focused field via ComposeRootHost.dispatchKeyEvent + the synthesised typed-key path. */
|
|
||||||
private fun runKeyTest() {
|
|
||||||
val vText = mutableStateOf("")
|
|
||||||
nativeComposeWindow(
|
|
||||||
title = "keytest",
|
|
||||||
width = 400,
|
|
||||||
height = 200,
|
|
||||||
onFrame = { _, frameIndex ->
|
|
||||||
when (frameIndex) {
|
|
||||||
12 -> { com.compose.sdl.injectMouseEvent(1, 200f, 100f); true } // click field to focus
|
|
||||||
14 -> { com.compose.sdl.injectMouseEvent(2, 200f, 100f); true }
|
|
||||||
24 -> { com.compose.sdl.injectTextInput("A"); true }
|
|
||||||
28 -> { com.compose.sdl.injectTextInput("B"); true }
|
|
||||||
32 -> { com.compose.sdl.injectKey(42, true); com.compose.sdl.injectKey(42, false); true } // Backspace → "A"
|
|
||||||
70 -> {
|
|
||||||
println("keytest: real BasicTextField value='${vText.value}'")
|
|
||||||
println(
|
|
||||||
if (vText.value == "A") "keytest: PASS (click-to-focus + type 'AB' + backspace = 'A')"
|
|
||||||
else "keytest: FAIL (expected 'A')"
|
|
||||||
)
|
|
||||||
false
|
|
||||||
}
|
|
||||||
else -> true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
// The exact regression path: a real BasicTextField, focused by clicking it,
|
|
||||||
// receiving typed text (SDL TEXT_INPUT) + editing keys (Backspace).
|
|
||||||
MaterialTheme(colorScheme = darkColorScheme()) {
|
|
||||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
|
||||||
androidx.compose.foundation.text.BasicTextField(
|
|
||||||
value = vText.value,
|
|
||||||
onValueChange = { vText.value = it },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Boots a tall Column wrapped in the vendored Modifier.verticalScroll, injects several wheel-down
|
|
||||||
events through the live SDL path (→ processor → MouseWheelScrollingLogic), and asserts the
|
|
||||||
ScrollState offset advanced - proving upstream scrolling works end-to-end. */
|
|
||||||
private fun runScrollTest() {
|
|
||||||
val vScroll = androidx.compose.foundation.ScrollState(0)
|
|
||||||
nativeComposeWindow(
|
|
||||||
title = "scrolltest",
|
|
||||||
width = 400,
|
|
||||||
height = 300,
|
|
||||||
onFrame = { _, frameIndex ->
|
|
||||||
when {
|
|
||||||
frameIndex in 20..40 && frameIndex % 2 == 0 -> {
|
|
||||||
com.compose.sdl.injectWheel(200f, 150f, 0f, -3f) // wheel down
|
|
||||||
true
|
|
||||||
}
|
|
||||||
frameIndex == 90 -> {
|
|
||||||
println("scrolltest: ScrollState.value=${vScroll.value} maxValue=${vScroll.maxValue}")
|
|
||||||
println(
|
|
||||||
if (vScroll.value > 0) "scrolltest: PASS (scrolled to ${vScroll.value}px)"
|
|
||||||
else "scrolltest: FAIL (did not scroll)"
|
|
||||||
)
|
|
||||||
false
|
|
||||||
}
|
|
||||||
else -> true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
MaterialTheme(colorScheme = darkColorScheme()) {
|
|
||||||
Column(modifier = Modifier.fillMaxSize().verticalScroll(vScroll)) {
|
|
||||||
repeat(40) { i ->
|
|
||||||
Text("Scroll row $i - lorem ipsum dolor sit", color = Color.White, fontSize = 16.sp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.SideEffect
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import com.compose.sdl.ComposeNativeWindow
|
||||||
|
import com.compose.sdl.nativeComposeWindow
|
||||||
|
|
||||||
|
/**
|
||||||
|
`--winattrtest`: opens a window with the non-default Compose Desktop attributes
|
||||||
|
set, then checks four things and exits -
|
||||||
|
|
||||||
|
1. the attributes reach SDL and read back off the facade,
|
||||||
|
2. the raw SDL escape hatch hands out the pointers the resolved renderer owns,
|
||||||
|
3. onPreviewKeyEvent sees a key BEFORE the focused content does,
|
||||||
|
4. `enabled = false` drops input entirely.
|
||||||
|
*/
|
||||||
|
fun runWindowAttrTest() {
|
||||||
|
var vPreviewSaw = 0
|
||||||
|
var vKeySaw = 0
|
||||||
|
var vFacade: ComposeNativeWindow? = null
|
||||||
|
var vFrames = 0
|
||||||
|
nativeComposeWindow(
|
||||||
|
title = "attr probe",
|
||||||
|
width = 420,
|
||||||
|
height = 240,
|
||||||
|
undecorated = true,
|
||||||
|
resizable = false,
|
||||||
|
alwaysOnTop = true,
|
||||||
|
focusable = true,
|
||||||
|
enabled = true,
|
||||||
|
// Returning false lets the event continue down the chain, so this also
|
||||||
|
// proves preview runs first without swallowing anything.
|
||||||
|
onPreviewKeyEvent = { vPreviewSaw++; false },
|
||||||
|
onKeyEvent = { vKeySaw++; false },
|
||||||
|
onFrame = { _, _ ->
|
||||||
|
vFrames++
|
||||||
|
// Let the first frames settle so SDL has applied everything.
|
||||||
|
if (vFrames < 3) return@nativeComposeWindow true
|
||||||
|
if (vFrames == 3) {
|
||||||
|
com.compose.sdl.injectKey(41, true) // Escape down
|
||||||
|
com.compose.sdl.injectKey(41, false)
|
||||||
|
return@nativeComposeWindow true
|
||||||
|
}
|
||||||
|
if (vFrames == 4) {
|
||||||
|
// Nothing is focused, so the content declines and BOTH window
|
||||||
|
// handlers must have seen the key.
|
||||||
|
println("KEYCHAIN preview=$vPreviewSaw onKey=$vKeySaw")
|
||||||
|
// Now disable the window and inject again: neither may fire.
|
||||||
|
vFacade?.setEnabled(false)
|
||||||
|
com.compose.sdl.injectKey(41, true)
|
||||||
|
com.compose.sdl.injectKey(41, false)
|
||||||
|
return@nativeComposeWindow true
|
||||||
|
}
|
||||||
|
if (vFrames == 5) {
|
||||||
|
println("DISABLED preview=$vPreviewSaw onKey=$vKeySaw (must be unchanged)")
|
||||||
|
vFacade?.setEnabled(true)
|
||||||
|
}
|
||||||
|
val vW = vFacade
|
||||||
|
if (vW == null) {
|
||||||
|
println("ATTR FAIL: window facade never captured")
|
||||||
|
} else {
|
||||||
|
println(
|
||||||
|
"ATTR undecorated=${vW.isUndecorated} resizable=${vW.isResizable} " +
|
||||||
|
"alwaysOnTop=${vW.isAlwaysOnTop} focusable=${vW.isFocusable} " +
|
||||||
|
"enabled=${vW.isEnabled}"
|
||||||
|
)
|
||||||
|
val vRaw = vW.rawSdlHandles()
|
||||||
|
println(
|
||||||
|
"RAW gpu=${vW.gpuMode} window=${vRaw.window != null} " +
|
||||||
|
"renderer=${vRaw.renderer != null} glContext=${vRaw.glContext != null} " +
|
||||||
|
"metalView=${vRaw.metalView != null}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
false
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
SideEffect { vFacade = window }
|
||||||
|
Box(Modifier.fillMaxSize()) { Text("attr probe") }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.darkColorScheme
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
|
import androidx.compose.ui.layout.positionInRoot
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.compose.sdl.nativeComposeWindow
|
||||||
|
import utils.encodeBmpBgra32
|
||||||
|
import utils.writeFile
|
||||||
|
|
||||||
|
// ==================
|
||||||
|
// MARK: Animation probes - shared transitions, AnimatedVisibility, dialog entry.
|
||||||
|
// ==================
|
||||||
|
//
|
||||||
|
// Split out of MainNative.kt, which held the entry point plus 20 scenarios in
|
||||||
|
// 1240 lines. Behaviour is unchanged; each probe is `internal` so main()'s
|
||||||
|
// argument dispatch still reaches it.
|
||||||
|
|
||||||
|
/** Drives a SharedTransitionLayout shared-element morph both directions from
|
||||||
|
the frame counter (no clicks) - it requires the LOOKAHEAD pass, which dies
|
||||||
|
with "LookaheadDelegate has not been measured yet" if the owner drops
|
||||||
|
affectsLookahead measure/relayout requests. Reaching the end frame = PASS. */
|
||||||
|
@OptIn(androidx.compose.animation.ExperimentalSharedTransitionApi::class)
|
||||||
|
internal fun runSharedTest() {
|
||||||
|
val vBig = mutableStateOf(false)
|
||||||
|
nativeComposeWindow(
|
||||||
|
title = "sharedtest",
|
||||||
|
width = 300,
|
||||||
|
height = 300,
|
||||||
|
onFrame = { _, vFrame ->
|
||||||
|
when (vFrame) {
|
||||||
|
20 -> { vBig.value = true; true } // small → big morph
|
||||||
|
70 -> { vBig.value = false; true } // big → small morph
|
||||||
|
130 -> {
|
||||||
|
println("sharedtest: PASS (shared-element morph ran both directions)")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
MaterialTheme(colorScheme = darkColorScheme()) {
|
||||||
|
androidx.compose.animation.SharedTransitionLayout {
|
||||||
|
Column {
|
||||||
|
androidx.compose.animation.AnimatedVisibility(visible = !vBig.value) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.sharedElement(
|
||||||
|
sharedContentState = rememberSharedContentState(key = "box"),
|
||||||
|
animatedVisibilityScope = this@AnimatedVisibility,
|
||||||
|
)
|
||||||
|
.size(48.dp)
|
||||||
|
.background(Color(0xFF7C4DFF), RoundedCornerShape(8.dp)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
androidx.compose.animation.AnimatedVisibility(visible = vBig.value) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.sharedElement(
|
||||||
|
sharedContentState = rememberSharedContentState(key = "box"),
|
||||||
|
animatedVisibilityScope = this@AnimatedVisibility,
|
||||||
|
)
|
||||||
|
.size(160.dp)
|
||||||
|
.background(Color(0xFF26A69A), RoundedCornerShape(24.dp)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Frame-by-frame trace of AnimatedVisibility(fadeIn+expandVertically / fadeOut+
|
||||||
|
shrinkVertically): logs the AV container's animated size and the window-Y of a
|
||||||
|
marker Box below it every frame, toggling visibility three times. Diagnoses
|
||||||
|
(a) a size snap between just-before-end and end of the animation and (b)
|
||||||
|
instant (non-animated) transitions on subsequent toggles.
|
||||||
|
|
||||||
|
Expected healthy trace: ~24 smooth frames per toggle ending exactly at 0/60,
|
||||||
|
PLUS one 16px marker jump when the fully-shrunk node unmounts (exit end) or
|
||||||
|
mounts (enter start) - that's the parent's spacedBy(16) collapsing, inherent
|
||||||
|
upstream behaviour (spacing applies to zero-height children too), NOT a bug. */
|
||||||
|
internal fun runAnimVisTest() {
|
||||||
|
val vShown = mutableStateOf(true)
|
||||||
|
var vAvSize = androidx.compose.ui.unit.IntSize(-1, -1)
|
||||||
|
var vMarkerY = -1f
|
||||||
|
var vLastLog = ""
|
||||||
|
nativeComposeWindow(
|
||||||
|
title = "animvistest",
|
||||||
|
width = 600,
|
||||||
|
height = 500,
|
||||||
|
onFrame = { _, vFrame ->
|
||||||
|
// Log only when something moved - keeps the trace readable.
|
||||||
|
val vLine = "size=$vAvSize markerY=$vMarkerY shown=${vShown.value}"
|
||||||
|
if (vLine != vLastLog) {
|
||||||
|
println("animvistest: f=$vFrame $vLine")
|
||||||
|
vLastLog = vLine
|
||||||
|
}
|
||||||
|
when (vFrame) {
|
||||||
|
60 -> { println("animvistest: === HIDE 1 ==="); vShown.value = false; true }
|
||||||
|
140 -> { println("animvistest: === SHOW 2 ==="); vShown.value = true; true }
|
||||||
|
220 -> { println("animvistest: === HIDE 3 ==="); vShown.value = false; true }
|
||||||
|
300 -> false
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
MaterialTheme(colorScheme = darkColorScheme()) {
|
||||||
|
// spacedBy mirrors the demo's Section column - the end-of-exit jump
|
||||||
|
// reported on the FoundationExtra screen involves the spacing around
|
||||||
|
// the AnimatedVisibility node collapsing when the node unmounts.
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
) {
|
||||||
|
Box(Modifier.size(30.dp).background(androidx.compose.ui.graphics.Color(0xFFC07040)))
|
||||||
|
androidx.compose.animation.AnimatedVisibility(
|
||||||
|
visible = vShown.value,
|
||||||
|
modifier = Modifier.onSizeChanged { vAvSize = it },
|
||||||
|
enter = androidx.compose.animation.fadeIn() + androidx.compose.animation.expandVertically(),
|
||||||
|
exit = androidx.compose.animation.fadeOut() + androidx.compose.animation.shrinkVertically(),
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(60.dp)
|
||||||
|
.background(androidx.compose.ui.graphics.Color(0xFF7040C0)),
|
||||||
|
) {
|
||||||
|
Text("Animated content", modifier = Modifier.padding(14.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(20.dp)
|
||||||
|
.background(androidx.compose.ui.graphics.Color(0xFF40C070))
|
||||||
|
.onGloballyPositioned { vMarkerY = it.positionInRoot().y },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Boots a window whose full-size surface opens an m3 AlertDialog on click (the real
|
||||||
|
material3 → ui.window.Dialog path, default DialogProperties ⇒ animateTransition on),
|
||||||
|
injects the click, then dumps screenshots during the 0.2s appearance animation and
|
||||||
|
after it settles; then injects Escape and dumps mid-disappearance (0.1s reverse,
|
||||||
|
via the popup host's exit deferral) + after. Visual check: mid shots must show the
|
||||||
|
dialog semi-transparent, slightly scaled-down and shifted down vs the settled shot,
|
||||||
|
and exit_end must show no dialog at all - skiko-parity animation both ways. */
|
||||||
|
internal fun runDialogAnimTest() {
|
||||||
|
fun snap(inBridge: com.compose.sdl.RenderBackend, inName: String) {
|
||||||
|
val vSnap = inBridge.snapshotBgra() ?: return
|
||||||
|
val (vW, vH, vBgra) = vSnap
|
||||||
|
writeFile(inName, encodeBmpBgra32(vW, vH, vBgra))
|
||||||
|
println("dialoganimtest: wrote $inName")
|
||||||
|
}
|
||||||
|
nativeComposeWindow(
|
||||||
|
title = "dialoganimtest",
|
||||||
|
width = 1000,
|
||||||
|
height = 700,
|
||||||
|
onFrame = { vBridge, vFrame ->
|
||||||
|
when (vFrame) {
|
||||||
|
30 -> { com.compose.sdl.injectMouseEvent(1, 500f, 350f); true }
|
||||||
|
32 -> { com.compose.sdl.injectMouseEvent(2, 500f, 350f); true }
|
||||||
|
36 -> { snap(vBridge, "dialog_mid1.bmp"); true }
|
||||||
|
40 -> { snap(vBridge, "dialog_mid2.bmp"); true }
|
||||||
|
90 -> { snap(vBridge, "dialog_end.bmp"); true }
|
||||||
|
92 -> {
|
||||||
|
com.compose.sdl.injectKey(41, true) // Escape → dismiss
|
||||||
|
com.compose.sdl.injectKey(41, false)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
97 -> { snap(vBridge, "dialog_exit_mid.bmp"); true }
|
||||||
|
140 -> { snap(vBridge, "dialog_exit_end.bmp"); false }
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
MaterialTheme(colorScheme = darkColorScheme()) {
|
||||||
|
var vShow by remember { mutableStateOf(false) }
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(MaterialTheme.colorScheme.background)
|
||||||
|
.clickable { vShow = true },
|
||||||
|
)
|
||||||
|
if (vShow) {
|
||||||
|
androidx.compose.material3.AlertDialog(
|
||||||
|
onDismissRequest = { vShow = false },
|
||||||
|
title = { Text("Animated dialog") },
|
||||||
|
text = { Text("Appearance animation parity with the JVM (skiko) Dialog.") },
|
||||||
|
confirmButton = {
|
||||||
|
androidx.compose.material3.TextButton(onClick = { vShow = false }) { Text("OK") }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,258 @@
|
|||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.darkColorScheme
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.input.pointer.PointerIcon
|
||||||
|
import androidx.compose.ui.input.pointer.pointerHoverIcon
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.compose.sdl.nativeComposeWindow
|
||||||
|
|
||||||
|
// ==================
|
||||||
|
// MARK: Input probes - pointer, keyboard, wheel, cursor and IME.
|
||||||
|
// ==================
|
||||||
|
//
|
||||||
|
// Split out of MainNative.kt, which held the entry point plus 20 scenarios in
|
||||||
|
// 1240 lines. Behaviour is unchanged; each probe is `internal` so main()'s
|
||||||
|
// argument dispatch still reaches it.
|
||||||
|
|
||||||
|
/** Boots a real nativeComposeWindow with a full-size clickable box, then injects
|
||||||
|
move→press→release SDL mouse events a few frames apart (giving the upstream
|
||||||
|
clickable's gesture coroutine time to launch + await between frames), and prints
|
||||||
|
PASS/FAIL based on whether onClick fired. Proves the whole vendored interaction
|
||||||
|
pipeline works under the real Sdl3MainDispatcher + frame loop. */
|
||||||
|
internal fun runClickTest() {
|
||||||
|
var vClicks = 0
|
||||||
|
nativeComposeWindow(
|
||||||
|
title = "clicktest",
|
||||||
|
width = 400,
|
||||||
|
height = 300,
|
||||||
|
onFrame = { _, frameIndex ->
|
||||||
|
when (frameIndex) {
|
||||||
|
20 -> { com.compose.sdl.injectMouseEvent(0, 200f, 150f); true }
|
||||||
|
26 -> { com.compose.sdl.injectMouseEvent(1, 200f, 150f); true }
|
||||||
|
32 -> { com.compose.sdl.injectMouseEvent(2, 200f, 150f); true }
|
||||||
|
70 -> {
|
||||||
|
println(
|
||||||
|
if (vClicks > 0) "clicktest: PASS ($vClicks click(s) via upstream clickable)"
|
||||||
|
else "clicktest: FAIL (0 clicks - upstream clickable did not fire)"
|
||||||
|
)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
MaterialTheme(colorScheme = darkColorScheme()) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color(0xFF303040))
|
||||||
|
.clickable { vClicks++; println("clicktest: onClick fired -> $vClicks") },
|
||||||
|
) {
|
||||||
|
Text("Click test", color = Color.White, fontSize = 16.sp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Same injection harness as clicktest, but the target is a Material Switch (Modifier.toggleable
|
||||||
|
from the vendored foundation.selection). Asserts onCheckedChange flipped the state - proving
|
||||||
|
toggleable rides the same verified upstream interaction path as clickable. */
|
||||||
|
internal fun runToggleTest() {
|
||||||
|
var vChecked = false
|
||||||
|
var vChanges = 0
|
||||||
|
nativeComposeWindow(
|
||||||
|
title = "toggletest",
|
||||||
|
width = 400,
|
||||||
|
height = 300,
|
||||||
|
onFrame = { _, frameIndex ->
|
||||||
|
when (frameIndex) {
|
||||||
|
20 -> { com.compose.sdl.injectMouseEvent(0, 200f, 150f); true }
|
||||||
|
26 -> { com.compose.sdl.injectMouseEvent(1, 200f, 150f); true }
|
||||||
|
32 -> { com.compose.sdl.injectMouseEvent(2, 200f, 150f); true }
|
||||||
|
70 -> {
|
||||||
|
println(
|
||||||
|
if (vChanges > 0 && vChecked) "toggletest: PASS (switch toggled to $vChecked via toggleable)"
|
||||||
|
else "toggletest: FAIL (changes=$vChanges checked=$vChecked)"
|
||||||
|
)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
MaterialTheme(colorScheme = darkColorScheme()) {
|
||||||
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
|
androidx.compose.material3.Switch(
|
||||||
|
checked = vChecked,
|
||||||
|
onCheckedChange = { vChecked = it; vChanges++; println("toggletest: onCheckedChange -> $it") },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Boots a window with a real BasicTextField, clicks it to focus (focus-on-click via the
|
||||||
|
FocusOwner), then injects TEXT_INPUT ("A","B") and a Backspace key through the live SDL path.
|
||||||
|
Asserts the field edits to "A" - proving click-to-focus + typing + editing keys route to the
|
||||||
|
focused field via ComposeRootHost.dispatchKeyEvent + the synthesised typed-key path. */
|
||||||
|
internal fun runKeyTest() {
|
||||||
|
val vText = mutableStateOf("")
|
||||||
|
nativeComposeWindow(
|
||||||
|
title = "keytest",
|
||||||
|
width = 400,
|
||||||
|
height = 200,
|
||||||
|
onFrame = { _, frameIndex ->
|
||||||
|
when (frameIndex) {
|
||||||
|
12 -> { com.compose.sdl.injectMouseEvent(1, 200f, 100f); true } // click field to focus
|
||||||
|
14 -> { com.compose.sdl.injectMouseEvent(2, 200f, 100f); true }
|
||||||
|
24 -> { com.compose.sdl.injectTextInput("A"); true }
|
||||||
|
28 -> { com.compose.sdl.injectTextInput("B"); true }
|
||||||
|
32 -> { com.compose.sdl.injectKey(42, true); com.compose.sdl.injectKey(42, false); true } // Backspace → "A"
|
||||||
|
70 -> {
|
||||||
|
println("keytest: real BasicTextField value='${vText.value}'")
|
||||||
|
println(
|
||||||
|
if (vText.value == "A") "keytest: PASS (click-to-focus + type 'AB' + backspace = 'A')"
|
||||||
|
else "keytest: FAIL (expected 'A')"
|
||||||
|
)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
// The exact regression path: a real BasicTextField, focused by clicking it,
|
||||||
|
// receiving typed text (SDL TEXT_INPUT) + editing keys (Backspace).
|
||||||
|
MaterialTheme(colorScheme = darkColorScheme()) {
|
||||||
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
|
androidx.compose.foundation.text.BasicTextField(
|
||||||
|
value = vText.value,
|
||||||
|
onValueChange = { vText.value = it },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Boots a tall Column wrapped in the vendored Modifier.verticalScroll, injects several wheel-down
|
||||||
|
events through the live SDL path (→ processor → MouseWheelScrollingLogic), and asserts the
|
||||||
|
ScrollState offset advanced - proving upstream scrolling works end-to-end. */
|
||||||
|
internal fun runScrollTest() {
|
||||||
|
val vScroll = androidx.compose.foundation.ScrollState(0)
|
||||||
|
nativeComposeWindow(
|
||||||
|
title = "scrolltest",
|
||||||
|
width = 400,
|
||||||
|
height = 300,
|
||||||
|
onFrame = { _, frameIndex ->
|
||||||
|
when {
|
||||||
|
frameIndex in 20..40 && frameIndex % 2 == 0 -> {
|
||||||
|
com.compose.sdl.injectWheel(200f, 150f, 0f, -3f) // wheel down
|
||||||
|
true
|
||||||
|
}
|
||||||
|
frameIndex == 90 -> {
|
||||||
|
println("scrolltest: ScrollState.value=${vScroll.value} maxValue=${vScroll.maxValue}")
|
||||||
|
println(
|
||||||
|
if (vScroll.value > 0) "scrolltest: PASS (scrolled to ${vScroll.value}px)"
|
||||||
|
else "scrolltest: FAIL (did not scroll)"
|
||||||
|
)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
MaterialTheme(colorScheme = darkColorScheme()) {
|
||||||
|
Column(modifier = Modifier.fillMaxSize().verticalScroll(vScroll)) {
|
||||||
|
repeat(40) { i ->
|
||||||
|
Text("Scroll row $i - lorem ipsum dolor sit", color = Color.White, fontSize = 16.sp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** --cursortest: injects hover moves over pointerHoverIcon(Text) / (Hand) regions
|
||||||
|
and a bare background, printing the applied SDL system cursor after each. Proves
|
||||||
|
PointerIcon -> SDL_SetCursor end-to-end through the live hover pipeline. */
|
||||||
|
internal fun runCursorTest() {
|
||||||
|
var vBg = "?"; var vText = "?"; var vHand = "?"
|
||||||
|
nativeComposeWindow(
|
||||||
|
title = "cursortest",
|
||||||
|
width = 400,
|
||||||
|
height = 400,
|
||||||
|
onFrame = { _, vFrame ->
|
||||||
|
when (vFrame) {
|
||||||
|
10 -> { com.compose.sdl.injectMouseEvent(0, 200f, 300f); true } // background (below both boxes)
|
||||||
|
14 -> { vBg = com.compose.sdl.appliedCursorName(); true }
|
||||||
|
20 -> { com.compose.sdl.injectMouseEvent(0, 200f, 50f); true } // Text box
|
||||||
|
24 -> { vText = com.compose.sdl.appliedCursorName(); true }
|
||||||
|
30 -> { com.compose.sdl.injectMouseEvent(0, 200f, 150f); true } // Hand box
|
||||||
|
34 -> {
|
||||||
|
vHand = com.compose.sdl.appliedCursorName()
|
||||||
|
println("cursortest: background=$vBg text=$vText hand=$vHand")
|
||||||
|
val vOk = vBg.endsWith("DEFAULT") && vText.endsWith("TEXT") && vHand.endsWith("POINTER")
|
||||||
|
println(if (vOk) "cursortest: PASS" else "cursortest: FAIL")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
MaterialTheme(colorScheme = darkColorScheme()) {
|
||||||
|
Column(Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background)) {
|
||||||
|
Box(Modifier.fillMaxWidth().height(100.dp).pointerHoverIcon(PointerIcon.Text))
|
||||||
|
Box(Modifier.fillMaxWidth().height(100.dp).pointerHoverIcon(PointerIcon.Hand))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** --imetest: focuses a BasicTextField, injects an IME composition (SDL TEXT_EDITING
|
||||||
|
"ni"), then a commit (SDL TEXT_INPUT "に"). Verifies the preedit shows a
|
||||||
|
composing region and the commit REPLACES it (not appends) - the real IME path. */
|
||||||
|
internal fun runImeTest() {
|
||||||
|
val vField = mutableStateOf(androidx.compose.ui.text.input.TextFieldValue(""))
|
||||||
|
nativeComposeWindow(
|
||||||
|
title = "imetest",
|
||||||
|
width = 500,
|
||||||
|
height = 200,
|
||||||
|
onFrame = { _, vFrame ->
|
||||||
|
when (vFrame) {
|
||||||
|
12 -> { com.compose.sdl.injectMouseEvent(1, 250f, 100f); true } // focus centered field
|
||||||
|
14 -> { com.compose.sdl.injectMouseEvent(2, 250f, 100f); true }
|
||||||
|
34 -> { com.compose.sdl.injectTextEditing("ni"); true } // composing (preedit)
|
||||||
|
40 -> { println("imetest: composing text='${vField.value.text}' composition=${vField.value.composition}"); true }
|
||||||
|
50 -> { com.compose.sdl.injectTextInput("に"); true } // commit "に"
|
||||||
|
60 -> {
|
||||||
|
println("imetest: committed text='${vField.value.text}' composition=${vField.value.composition}")
|
||||||
|
val vOk = vField.value.text == "に" && vField.value.composition == null
|
||||||
|
println(if (vOk) "imetest: PASS" else "imetest: FAIL")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
MaterialTheme(colorScheme = darkColorScheme()) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
androidx.compose.foundation.text.BasicTextField(
|
||||||
|
value = vField.value,
|
||||||
|
onValueChange = { vField.value = it },
|
||||||
|
textStyle = androidx.compose.ui.text.TextStyle(color = Color.White, fontSize = 24.sp),
|
||||||
|
cursorBrush = androidx.compose.ui.graphics.SolidColor(Color.White),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.darkColorScheme
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.compose.sdl.nativeComposeWindow
|
||||||
|
import utils.encodeBmpBgra32
|
||||||
|
import utils.writeFile
|
||||||
|
|
||||||
|
// ==================
|
||||||
|
// MARK: Lifecycle probes - back navigation and Navigation3.
|
||||||
|
// ==================
|
||||||
|
//
|
||||||
|
// Split out of MainNative.kt, which held the entry point plus 20 scenarios in
|
||||||
|
// 1240 lines. Behaviour is unchanged; each probe is `internal` so main()'s
|
||||||
|
// argument dispatch still reaches it.
|
||||||
|
|
||||||
|
/** Boots a window with a BackHandler, injects an Escape key through the live SDL
|
||||||
|
path, and asserts the handler fired - proving ComposeWindow's
|
||||||
|
BackNavigationInput drives the NavigationEventDispatcher (the mechanism that
|
||||||
|
collapses an expanded m3 SearchBar on Escape). */
|
||||||
|
@OptIn(androidx.compose.ui.ExperimentalComposeUiApi::class)
|
||||||
|
internal fun runBackTest() {
|
||||||
|
val vBackNoFocus = mutableStateOf(false)
|
||||||
|
val vBackFocused = mutableStateOf(false)
|
||||||
|
val vText = mutableStateOf("")
|
||||||
|
nativeComposeWindow(
|
||||||
|
title = "backtest",
|
||||||
|
width = 400,
|
||||||
|
height = 200,
|
||||||
|
onFrame = { _, frameIndex ->
|
||||||
|
when (frameIndex) {
|
||||||
|
// Phase 1: Escape with NOTHING focused.
|
||||||
|
20 -> {
|
||||||
|
com.compose.sdl.injectKey(41, true) // SDL_SCANCODE_ESCAPE
|
||||||
|
com.compose.sdl.injectKey(41, false)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
// Phase 2: click the text field to focus it, then Escape -
|
||||||
|
// the user-facing SearchBar scenario (field focused while
|
||||||
|
// the back handler should collapse the bar).
|
||||||
|
30 -> { com.compose.sdl.injectMouseEvent(1, 200f, 100f); true }
|
||||||
|
32 -> { com.compose.sdl.injectMouseEvent(2, 200f, 100f); true }
|
||||||
|
44 -> {
|
||||||
|
com.compose.sdl.injectKey(41, true)
|
||||||
|
com.compose.sdl.injectKey(41, false)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
70 -> {
|
||||||
|
println("backtest: noFocus=${vBackNoFocus.value} focused=${vBackFocused.value}")
|
||||||
|
println(
|
||||||
|
if (vBackNoFocus.value && vBackFocused.value)
|
||||||
|
"backtest: PASS (Escape completed back navigation with and without a focused field)"
|
||||||
|
else
|
||||||
|
"backtest: FAIL (noFocus=${vBackNoFocus.value} focusedField=${vBackFocused.value})"
|
||||||
|
)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
androidx.compose.ui.backhandler.BackHandler(enabled = true) {
|
||||||
|
if (!vBackNoFocus.value) vBackNoFocus.value = true else vBackFocused.value = true
|
||||||
|
}
|
||||||
|
MaterialTheme(colorScheme = darkColorScheme()) {
|
||||||
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
|
// The NEW state-based field - the one m3 SearchBar's InputField
|
||||||
|
// uses - so the probe exercises its key handler, not the legacy
|
||||||
|
// value-based one.
|
||||||
|
androidx.compose.foundation.text.BasicTextField(
|
||||||
|
state = androidx.compose.foundation.text.input.rememberTextFieldState(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun runNav3Test() {
|
||||||
|
val vShow = mutableStateOf(false)
|
||||||
|
nativeComposeWindow(
|
||||||
|
title = "nav3test",
|
||||||
|
width = 1000,
|
||||||
|
height = 700,
|
||||||
|
onFrame = { vBridge, vFrame ->
|
||||||
|
when (vFrame) {
|
||||||
|
30 -> { vShow.value = true; true }
|
||||||
|
// Push Detail #1 (click its card) → per-entry lifecycle goes
|
||||||
|
// CREATED/STARTED during the slide, RESUMED once settled.
|
||||||
|
60 -> { com.compose.sdl.injectMouseEvent(1, 500f, 216f); true }
|
||||||
|
62 -> { com.compose.sdl.injectMouseEvent(2, 500f, 216f); true }
|
||||||
|
// Pop with ESC → ON_PAUSE, then CREATED while animating out,
|
||||||
|
// then the entry disposes.
|
||||||
|
120 -> {
|
||||||
|
com.compose.sdl.injectKey(41, true)
|
||||||
|
com.compose.sdl.injectKey(41, false)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
180 -> {
|
||||||
|
val vSnap = vBridge.snapshotBgra()
|
||||||
|
if (vSnap != null) {
|
||||||
|
val (vW, vH, vBgra) = vSnap
|
||||||
|
writeFile("nav3late.bmp", encodeBmpBgra32(vW, vH, vBgra))
|
||||||
|
println("nav3test: wrote nav3late.bmp (${vW}x${vH})")
|
||||||
|
println("nav3test: PASS (Navigation3 composed at RESUMED; push + pop ran)")
|
||||||
|
} else println("nav3test: FAIL (no snapshot)")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
MaterialTheme(colorScheme = darkColorScheme()) {
|
||||||
|
Box(modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background)) {
|
||||||
|
if (vShow.value) {
|
||||||
|
Box(modifier = Modifier.padding(24.dp)) { screens.Navigation3Screen() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.darkColorScheme
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.decodeToImageBitmap
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.compose.sdl.nativeComposeWindow
|
||||||
|
import demo.registry.allCategories
|
||||||
|
import kotlinx.cinterop.ExperimentalForeignApi
|
||||||
|
import kotlinx.cinterop.toKString
|
||||||
|
|
||||||
|
// ==================
|
||||||
|
// MARK: Renderer probes - the soak run and encoded-image decoding.
|
||||||
|
// ==================
|
||||||
|
//
|
||||||
|
// Split out of MainNative.kt, which held the entry point plus 20 scenarios in
|
||||||
|
// 1240 lines. Behaviour is unchanged; each probe is `internal` so main()'s
|
||||||
|
// argument dispatch still reaches it.
|
||||||
|
|
||||||
|
/** P2.2 soak - cycle through EVERY registered screen kCycles times in ONE process,
|
||||||
|
disposing each via a changing key() so composition + layer (skiko RenderNode)
|
||||||
|
allocate-and-release is exercised repeatedly. After each full cycle, GC then
|
||||||
|
record peak RSS (getrusage ru_maxrss). Peak RSS is monotonic, so after cycle 1 it
|
||||||
|
already reflects visiting every screen once; if there is NO leak it plateaus, if
|
||||||
|
there IS one it keeps climbing each cycle. PASS iff last-cycle peak stays within a
|
||||||
|
ceiling of the first-cycle peak. */
|
||||||
|
@OptIn(kotlin.native.runtime.NativeRuntimeApi::class, ExperimentalForeignApi::class)
|
||||||
|
internal fun runSoakTest() {
|
||||||
|
// Disable never-settling animations so RSS reflects composition/layer lifetime,
|
||||||
|
// not live animation-state churn (same seed the screenshot path uses).
|
||||||
|
com.compose.sdl.disableInfiniteAnimations = true
|
||||||
|
com.compose.sdl.useVirtualFrameTime = true
|
||||||
|
val vAll = allCategories().flatMap { it.screens }
|
||||||
|
// CDN_SOAK_SCREEN=<name> repeats ONE screen 40x/cycle (bisect a specific screen);
|
||||||
|
// default cycles through every screen.
|
||||||
|
val vTarget = platform.posix.getenv("CDN_SOAK_SCREEN")?.toKString()
|
||||||
|
val vScreens = if (vTarget != null) { val vS = vAll.first { it.name.equals(vTarget, ignoreCase = true) }; List(40) { vS } }
|
||||||
|
else vAll
|
||||||
|
val vIndex = mutableStateOf(0)
|
||||||
|
// CDN_SOAK_STATIC=1: mount ONE screen once, never remount - measure RSS every 120 frames.
|
||||||
|
// Isolates a per-FRAME leak (RSS climbs with no remounts) from a per-MOUNT leak.
|
||||||
|
val vStatic = platform.posix.getenv("CDN_SOAK_STATIC")?.toKString() == "1"
|
||||||
|
val kFramesPerScreen = if (vStatic) 120 else 3
|
||||||
|
val kCycles = platform.posix.getenv("CDN_SOAK_CYCLES")?.toKString()?.toIntOrNull() ?: 3
|
||||||
|
val vRssPerCycleMb = mutableListOf<Long>()
|
||||||
|
var vShown = 0
|
||||||
|
|
||||||
|
nativeComposeWindow(
|
||||||
|
title = "soaktest",
|
||||||
|
width = 1000,
|
||||||
|
height = 700,
|
||||||
|
onFrame = { _, vFrame ->
|
||||||
|
if (vFrame > 0 && vFrame % kFramesPerScreen == 0) {
|
||||||
|
if (vStatic) {
|
||||||
|
// No remount - just pump frames on the one mounted screen.
|
||||||
|
kotlin.native.runtime.GC.collect()
|
||||||
|
vRssPerCycleMb.add(currentResidentMb())
|
||||||
|
println("soaktest[static]: measure ${vRssPerCycleMb.size}: currentRSS=${vRssPerCycleMb.last()}MB")
|
||||||
|
} else {
|
||||||
|
vShown++
|
||||||
|
vIndex.value = vShown % vScreens.size
|
||||||
|
if (vShown % vScreens.size == 0) {
|
||||||
|
kotlin.native.runtime.GC.collect()
|
||||||
|
vRssPerCycleMb.add(currentResidentMb())
|
||||||
|
println("soaktest: cycle ${vRssPerCycleMb.size}: currentRSS=${vRssPerCycleMb.last()}MB")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (vRssPerCycleMb.size >= kCycles) {
|
||||||
|
val vFirst = vRssPerCycleMb.first()
|
||||||
|
val vLast = vRssPerCycleMb.last()
|
||||||
|
val vGrowth = vLast - vFirst
|
||||||
|
val vCeiling = maxOf(48L, vFirst / 4) // 25% or 48MB, whichever larger
|
||||||
|
println("soaktest: currentRSS/cycle(MB)=$vRssPerCycleMb (${vScreens.size} screens x $kCycles cycles)")
|
||||||
|
if (vGrowth <= vCeiling) {
|
||||||
|
println("soaktest: PASS (current RSS grew ${vGrowth}MB over cycles 1->$kCycles, within ${vCeiling}MB ceiling)")
|
||||||
|
} else {
|
||||||
|
println("soaktest: FAIL (current RSS grew ${vGrowth}MB > ${vCeiling}MB ceiling - possible leak)")
|
||||||
|
}
|
||||||
|
false
|
||||||
|
} else true
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
MaterialTheme(colorScheme = darkColorScheme()) {
|
||||||
|
val vScroll = rememberScrollState()
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(MaterialTheme.colorScheme.background)
|
||||||
|
.verticalScroll(vScroll)
|
||||||
|
.padding(24.dp),
|
||||||
|
) {
|
||||||
|
// key(vShown) forces a FULL dispose+recompose each advance (even when the
|
||||||
|
// target screen repeats) - the allocate/release churn we soak. Reading
|
||||||
|
// vIndex.value (a State) is what triggers the recomposition each mount.
|
||||||
|
key(vShown) {
|
||||||
|
vScreens[vIndex.value].content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** CURRENT resident set (MB): current RSS can DROP after GC, so it distinguishes a
|
||||||
|
true leak (ratchets up) from K/N allocator high-water, unlike getrusage's
|
||||||
|
monotonic peak. posix reads it from `ps`; mingw has no `ps`/popen and returns
|
||||||
|
-1 (the soak gate runs on macOS/Linux - see scripts/verify-mac.sh). */
|
||||||
|
internal expect fun currentResidentMb(): Long
|
||||||
|
|
||||||
|
/** --imagebytestest: decodes an in-memory 2x2 BMP via ByteArray.decodeToImageBitmap()
|
||||||
|
(routes to createImageBitmap(bytes)) through Skia's image decoder. Needs a window
|
||||||
|
so the render backend installs the encoded-image decoder. */
|
||||||
|
internal fun runImageBytesTest() {
|
||||||
|
nativeComposeWindow(
|
||||||
|
title = "imagebytestest",
|
||||||
|
width = 200,
|
||||||
|
height = 200,
|
||||||
|
onFrame = { _, vFrame ->
|
||||||
|
if (vFrame >= 2) {
|
||||||
|
try {
|
||||||
|
val vBitmap = tinyRedBmp().decodeToImageBitmap()
|
||||||
|
println("imagebytestest: decoded ${vBitmap.width}x${vBitmap.height}")
|
||||||
|
println(if (vBitmap.width == 2 && vBitmap.height == 2) "imagebytestest: PASS" else "imagebytestest: FAIL (wrong size)")
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
println("imagebytestest: FAIL (${e::class.simpleName}: ${e.message})")
|
||||||
|
}
|
||||||
|
false
|
||||||
|
} else true
|
||||||
|
},
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A minimal 2x2 24bpp red BMP (no compression) - valid decoder input.
|
||||||
|
internal fun tinyRedBmp(): ByteArray {
|
||||||
|
fun le16(v: Int) = byteArrayOf((v and 0xFF).toByte(), ((v shr 8) and 0xFF).toByte())
|
||||||
|
fun le32(v: Int) = byteArrayOf(
|
||||||
|
(v and 0xFF).toByte(), ((v shr 8) and 0xFF).toByte(),
|
||||||
|
((v shr 16) and 0xFF).toByte(), ((v shr 24) and 0xFF).toByte(),
|
||||||
|
)
|
||||||
|
val vPixel = byteArrayOf(0, 0, 0xFF.toByte()) // BGR red
|
||||||
|
val vRow = vPixel + vPixel + byteArrayOf(0, 0) // 2px + 2 pad = 8 bytes (4-byte aligned)
|
||||||
|
val vPixels = vRow + vRow // 2 rows = 16 bytes
|
||||||
|
val vFileHeader = byteArrayOf('B'.code.toByte(), 'M'.code.toByte()) + le32(70) + le32(0) + le32(54)
|
||||||
|
val vDib = le32(40) + le32(2) + le32(2) + le16(1) + le16(24) + le32(0) + le32(16) +
|
||||||
|
le32(2835) + le32(2835) + le32(0) + le32(0)
|
||||||
|
return vFileHeader + vDib + vPixels
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.darkColorScheme
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.compose.sdl.nativeComposeWindow
|
||||||
|
import utils.encodeBmpBgra32
|
||||||
|
import utils.writeFile
|
||||||
|
|
||||||
|
// ==================
|
||||||
|
// MARK: Text probes - font resolution, paragraph layout, metrics.
|
||||||
|
// ==================
|
||||||
|
//
|
||||||
|
// Split out of MainNative.kt, which held the entry point plus 20 scenarios in
|
||||||
|
// 1240 lines. Behaviour is unchanged; each probe is `internal` so main()'s
|
||||||
|
// argument dispatch still reaches it.
|
||||||
|
|
||||||
|
/** --fonttest: same sample in FontFamily.Default (sans) and FontFamily.Monospace.
|
||||||
|
Monospace used to collapse to the default sans; now it renders NotoSansMono
|
||||||
|
(bundled because this source references FontFamily.Monospace). */
|
||||||
|
internal fun runFontTest() {
|
||||||
|
nativeComposeWindow(
|
||||||
|
title = "fonttest",
|
||||||
|
width = 560,
|
||||||
|
height = 220,
|
||||||
|
onFrame = { vBridge, vFrame ->
|
||||||
|
if (vFrame >= 10) {
|
||||||
|
val vSnap = vBridge.snapshotBgra()
|
||||||
|
if (vSnap != null) {
|
||||||
|
val (vW, vH, vBgra) = vSnap
|
||||||
|
writeFile("fonttest.bmp", encodeBmpBgra32(vW, vH, vBgra))
|
||||||
|
println("fonttest: wrote fonttest.bmp (${vW}x${vH})")
|
||||||
|
}
|
||||||
|
false
|
||||||
|
} else true
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
MaterialTheme(colorScheme = darkColorScheme()) {
|
||||||
|
Column(Modifier.fillMaxSize().background(Color(0xFF202020)).padding(20.dp)) {
|
||||||
|
val vSample = "Illegal1 lIO0 {}=>"
|
||||||
|
Text(vSample, color = Color.White, fontSize = 30.sp)
|
||||||
|
Text(
|
||||||
|
vSample, color = Color.Cyan, fontSize = 30.sp,
|
||||||
|
fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Boots a real window, then builds an upstream Paragraph via the
|
||||||
|
vendored factory (→ SkiaParagraph) for a long string constrained to a narrow width. Verifies it
|
||||||
|
wrapped to multiple lines, has positive size, and that getHorizontalPosition/getOffsetForPosition
|
||||||
|
round-trip - proving the paragraph-engine measurement bridge works. */
|
||||||
|
internal fun runParagraphTest() {
|
||||||
|
nativeComposeWindow(
|
||||||
|
title = "paragraphtest",
|
||||||
|
width = 400,
|
||||||
|
height = 200,
|
||||||
|
onFrame = { _, frameIndex ->
|
||||||
|
if (frameIndex == 10) {
|
||||||
|
val vP = androidx.compose.ui.text.Paragraph(
|
||||||
|
text = "Hello world foo bar baz qux quux corge grault garply waldo",
|
||||||
|
style = androidx.compose.ui.text.TextStyle(fontSize = 16.sp),
|
||||||
|
constraints = androidx.compose.ui.unit.Constraints(maxWidth = 120),
|
||||||
|
density = androidx.compose.ui.unit.Density(1f),
|
||||||
|
fontFamilyResolver = androidx.compose.ui.text.font.createFontFamilyResolver(),
|
||||||
|
overflow = androidx.compose.ui.text.style.TextOverflow.Clip,
|
||||||
|
)
|
||||||
|
val vHpos = vP.getHorizontalPosition(3, true)
|
||||||
|
val vOffBack = vP.getOffsetForPosition(androidx.compose.ui.geometry.Offset(vHpos, 2f))
|
||||||
|
val vLine = vP.getLineForOffset(30)
|
||||||
|
println("paragraphtest: lineCount=${vP.lineCount} w=${vP.width} h=${vP.height} hpos(3)=$vHpos offBack=$vOffBack lineFor(30)=$vLine")
|
||||||
|
val vPass = vP.lineCount >= 2 && vP.height > 0f && vP.width > 0f && vOffBack in 2..4
|
||||||
|
println(if (vPass) "paragraphtest: PASS (real width-wrapped Paragraph via SkiaParagraph)" else "paragraphtest: FAIL")
|
||||||
|
false
|
||||||
|
} else true
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Box(modifier = Modifier.fillMaxSize()) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Prints paragraph metrics (single-line cell, lineHeight-styled single + triple line,
|
||||||
|
first baseline) for a font-size sweep at density 1 - the native half of the
|
||||||
|
metrics-alignment probe. Mirror of MainJvm's `--metrics`; both must print the same
|
||||||
|
numbers for the parity text drift to vanish. */
|
||||||
|
internal fun runMetricsProbe() {
|
||||||
|
fun paragraph(inText: String, inSize: Int, inLineHeight: Int?, inM3Style: Boolean): androidx.compose.ui.text.Paragraph =
|
||||||
|
androidx.compose.ui.text.Paragraph(
|
||||||
|
text = inText,
|
||||||
|
style = androidx.compose.ui.text.TextStyle(
|
||||||
|
fontSize = inSize.sp,
|
||||||
|
lineHeight = inLineHeight?.sp ?: androidx.compose.ui.unit.TextUnit.Unspecified,
|
||||||
|
lineHeightStyle = if (inM3Style) {
|
||||||
|
androidx.compose.ui.text.style.LineHeightStyle(
|
||||||
|
alignment = androidx.compose.ui.text.style.LineHeightStyle.Alignment.Center,
|
||||||
|
trim = androidx.compose.ui.text.style.LineHeightStyle.Trim.None,
|
||||||
|
)
|
||||||
|
} else null,
|
||||||
|
),
|
||||||
|
constraints = androidx.compose.ui.unit.Constraints(maxWidth = 10_000),
|
||||||
|
density = androidx.compose.ui.unit.Density(1f),
|
||||||
|
fontFamilyResolver = androidx.compose.ui.text.font.createFontFamilyResolver(),
|
||||||
|
overflow = androidx.compose.ui.text.style.TextOverflow.Clip,
|
||||||
|
)
|
||||||
|
nativeComposeWindow(
|
||||||
|
title = "metricsprobe",
|
||||||
|
width = 300,
|
||||||
|
height = 200,
|
||||||
|
onFrame = { _, frameIndex ->
|
||||||
|
if (frameIndex == 10) {
|
||||||
|
for (vSize in listOf(11, 12, 14, 16, 22, 24)) {
|
||||||
|
val vLh = vSize + 6
|
||||||
|
val vCell = paragraph("Hg", vSize, null, false)
|
||||||
|
val vOne = paragraph("Hg", vSize, vLh, false)
|
||||||
|
val vThree = paragraph("Hg\nHg\nHg", vSize, vLh, false)
|
||||||
|
val vOneM3 = paragraph("Hg", vSize, vLh, true)
|
||||||
|
val vThreeM3 = paragraph("Hg\nHg\nHg", vSize, vLh, true)
|
||||||
|
println(
|
||||||
|
"metrics: size=$vSize lh=$vLh cell=${vCell.height} " +
|
||||||
|
"one=${vOne.height} three=${vThree.height} " +
|
||||||
|
"base1=${vOne.firstBaseline} base3=${vThree.lastBaseline} " +
|
||||||
|
"oneM3=${vOneM3.height} threeM3=${vThreeM3.height} base1M3=${vOneM3.firstBaseline}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
for ((vS, vL) in listOf(24 to 24, 24 to 25, 32 to 24, 16 to 16)) {
|
||||||
|
val vB = paragraph("Hg", vS, vL, true)
|
||||||
|
println("metrics: boundary $vS/$vL m3=${vB.height} base=${vB.firstBaseline}")
|
||||||
|
}
|
||||||
|
val vBig = paragraph("42", 48, 24, false)
|
||||||
|
val vBigM3 = paragraph("42", 48, 24, true)
|
||||||
|
println("metrics: big48/lh24 raw=${vBig.height} base=${vBig.firstBaseline} m3=${vBigM3.height} baseM3=${vBigM3.firstBaseline}")
|
||||||
|
println("metricsprobe: DONE")
|
||||||
|
false
|
||||||
|
} else true
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Box(modifier = Modifier.fillMaxSize()) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================
|
||||||
|
// MARK: clicktest
|
||||||
|
// ==================
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.darkColorScheme
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.compose.sdl.Window
|
||||||
|
import com.compose.sdl.nativeComposeApp
|
||||||
|
import com.compose.sdl.nativeComposeWindow
|
||||||
|
import utils.encodeBmpBgra32
|
||||||
|
import utils.writeFile
|
||||||
|
|
||||||
|
// ==================
|
||||||
|
// MARK: Window probes - multi-window lifecycle, window info, locale.
|
||||||
|
// ==================
|
||||||
|
//
|
||||||
|
// Split out of MainNative.kt, which held the entry point plus 20 scenarios in
|
||||||
|
// 1240 lines. Behaviour is unchanged; each probe is `internal` so main()'s
|
||||||
|
// argument dispatch still reaches it.
|
||||||
|
|
||||||
|
/** Boots TWO windows via nativeComposeApp, asserts both render, closes the
|
||||||
|
second by flipping the state that composes its Window(), and asserts the app
|
||||||
|
keeps running on the first - the multi-window lifecycle end-to-end. */
|
||||||
|
internal fun runMultiWindowTest() {
|
||||||
|
val vShowSecond = mutableStateOf(true)
|
||||||
|
var vSecondFrames = 0
|
||||||
|
var vResult = "FAIL (main window never reached the end frame)"
|
||||||
|
nativeComposeApp {
|
||||||
|
Window(
|
||||||
|
onCloseRequest = ::exitApplication,
|
||||||
|
title = "multiwin main",
|
||||||
|
width = 400,
|
||||||
|
height = 200,
|
||||||
|
onFrame = { _, vFrame ->
|
||||||
|
when (vFrame) {
|
||||||
|
40 -> {
|
||||||
|
if (vSecondFrames == 0) {
|
||||||
|
vResult = "FAIL (second window never rendered)"
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
vShowSecond.value = false // close the second window via state
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
80 -> {
|
||||||
|
vResult =
|
||||||
|
if (!vShowSecond.value && vSecondFrames > 0)
|
||||||
|
"PASS (both windows rendered; second closed via state; app survived on the first)"
|
||||||
|
else "FAIL (second=$vSecondFrames showSecond=${vShowSecond.value})"
|
||||||
|
false
|
||||||
|
}
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Text("main window", color = Color.White)
|
||||||
|
}
|
||||||
|
if (vShowSecond.value) {
|
||||||
|
Window(
|
||||||
|
onCloseRequest = { vShowSecond.value = false },
|
||||||
|
title = "multiwin second",
|
||||||
|
width = 300,
|
||||||
|
height = 150,
|
||||||
|
onFrame = { _, _ -> vSecondFrames++; true },
|
||||||
|
) {
|
||||||
|
Text("second window", color = Color.White)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println("multiwintest: secondFrames=$vSecondFrames")
|
||||||
|
println("multiwintest: $vResult")
|
||||||
|
}
|
||||||
|
|
||||||
|
/** --windowinfotest: prints LocalWindowInfo.isWindowFocused / containerSize /
|
||||||
|
containerDpSize. containerSize starts Zero and becomes the window pixel size
|
||||||
|
after the first measure - proof it's fed from the live root constraints. */
|
||||||
|
internal fun runWindowInfoTest() {
|
||||||
|
nativeComposeWindow(
|
||||||
|
title = "windowinfotest",
|
||||||
|
width = 400,
|
||||||
|
height = 300,
|
||||||
|
onFrame = { _, vFrame -> vFrame < 6 },
|
||||||
|
) {
|
||||||
|
val vWi = androidx.compose.ui.platform.LocalWindowInfo.current
|
||||||
|
androidx.compose.runtime.LaunchedEffect(vWi.containerSize, vWi.isWindowFocused) {
|
||||||
|
println("windowinfotest: focused=${vWi.isWindowFocused} containerSize=${vWi.containerSize} containerDpSize=${vWi.containerDpSize}")
|
||||||
|
}
|
||||||
|
MaterialTheme(colorScheme = darkColorScheme()) {
|
||||||
|
Box(Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Boots the REAL Search screen, clicks the first SearchBar's input field
|
||||||
|
(expands it), presses Escape, and writes esc_before.bmp / esc_after.bmp -
|
||||||
|
the expanded overlay must be visible in `before` and gone in `after`. */
|
||||||
|
internal fun runSearchEscTest() {
|
||||||
|
fun snap(inBridge: com.compose.sdl.RenderBackend, inName: String) {
|
||||||
|
val vSnap = inBridge.snapshotBgra() ?: return
|
||||||
|
val (vW, vH, vBgra) = vSnap
|
||||||
|
writeFile(inName, encodeBmpBgra32(vW, vH, vBgra))
|
||||||
|
println("searchesctest: wrote $inName")
|
||||||
|
}
|
||||||
|
nativeComposeWindow(
|
||||||
|
title = "searchesctest",
|
||||||
|
width = 1000,
|
||||||
|
height = 700,
|
||||||
|
onFrame = { vBridge, vFrame ->
|
||||||
|
when (vFrame) {
|
||||||
|
30 -> { com.compose.sdl.injectMouseEvent(1, 200f, 230f); true }
|
||||||
|
32 -> { com.compose.sdl.injectMouseEvent(2, 200f, 230f); true }
|
||||||
|
70 -> { snap(vBridge, "esc_before.bmp"); true }
|
||||||
|
80 -> {
|
||||||
|
com.compose.sdl.injectKey(41, true) // Escape
|
||||||
|
com.compose.sdl.injectKey(41, false)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
130 -> { snap(vBridge, "esc_after.bmp"); false }
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
MaterialTheme(colorScheme = darkColorScheme()) {
|
||||||
|
val vScroll = rememberScrollState()
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(MaterialTheme.colorScheme.background)
|
||||||
|
.verticalScroll(vScroll)
|
||||||
|
.padding(24.dp),
|
||||||
|
) {
|
||||||
|
screens.M3SearchScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Boots an empty window, then composes screens.Navigation3Screen() at frame 30 -
|
||||||
|
AFTER the window lifecycle reached RESUMED - mirroring the real sidebar flow.
|
||||||
|
Crashes here (e.g. enableSavedStateHandles' INITIALIZED/CREATED contract) never
|
||||||
|
reproduce under --screen, which composes during the initial CREATED composition.
|
||||||
|
PASS = a screenshot gets written and the app exits cleanly. */
|
||||||
|
|
||||||
|
/** --localetest: prints Locale.current / LocaleList.current (should reflect the OS
|
||||||
|
preferred locales via SDL) and screenshots an M3 DatePicker, whose headline and
|
||||||
|
navigation labels are M3-translated by Locale.current. Run under a forced locale,
|
||||||
|
e.g. `demo.kexe --localetest -AppleLanguages "(fr-FR)"` on macOS. */
|
||||||
|
@OptIn(androidx.compose.material3.ExperimentalMaterial3Api::class)
|
||||||
|
internal fun runLocaleTest() {
|
||||||
|
nativeComposeWindow(
|
||||||
|
title = "localetest",
|
||||||
|
width = 420,
|
||||||
|
height = 560,
|
||||||
|
onFrame = { vBridge, vFrame ->
|
||||||
|
when (vFrame) {
|
||||||
|
2 -> {
|
||||||
|
val vCur = androidx.compose.ui.text.intl.Locale.current
|
||||||
|
val vList = androidx.compose.ui.text.intl.LocaleList.current
|
||||||
|
println("localetest: Locale.current=${vCur.toLanguageTag()}")
|
||||||
|
println("localetest: LocaleList.current=[${vList.localeList.joinToString { it.toLanguageTag() }}]")
|
||||||
|
true
|
||||||
|
}
|
||||||
|
40 -> {
|
||||||
|
val vSnap = vBridge.snapshotBgra()
|
||||||
|
if (vSnap != null) {
|
||||||
|
val (vW, vH, vBgra) = vSnap
|
||||||
|
writeFile("localetest.bmp", encodeBmpBgra32(vW, vH, vBgra))
|
||||||
|
println("localetest: wrote localetest.bmp (${vW}x${vH})")
|
||||||
|
} else println("localetest: FAIL (no snapshot)")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
MaterialTheme(colorScheme = darkColorScheme()) {
|
||||||
|
androidx.compose.material3.Surface(modifier = Modifier.fillMaxSize()) {
|
||||||
|
val vState = androidx.compose.material3.rememberDatePickerState()
|
||||||
|
androidx.compose.material3.DatePicker(state = vState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,10 +22,10 @@ import com.compose.sdl.LocalComposeNativeWindow
|
|||||||
own id, so closing one removes exactly THAT id (a count would always drop the
|
own id, so closing one removes exactly THAT id (a count would always drop the
|
||||||
last-declared window, closing the wrong one). */
|
last-declared window, closing the wrong one). */
|
||||||
val ExtraWindows = mutableStateListOf<Int>()
|
val ExtraWindows = mutableStateListOf<Int>()
|
||||||
private var fNextExtraWindowId = 1
|
private var mNextExtraWindowId = 1
|
||||||
|
|
||||||
fun openExtraWindow() {
|
fun openExtraWindow() {
|
||||||
ExtraWindows.add(fNextExtraWindowId++)
|
ExtraWindows.add(mNextExtraWindowId++)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import com.compose.sdl.GpuMode
|
|||||||
|
|
||||||
/** Parsed view of the demo's command line.
|
/** Parsed view of the demo's command line.
|
||||||
|
|
||||||
--gpu=auto | software | skia.metal | skia.opengl (default: auto)
|
--gpu=auto | cpu | metal | opengl (default: auto)
|
||||||
--screen=Buttons | TextField | ... (default: full app w/ sidebar)
|
--screen=Buttons | TextField | ... (default: full app w/ sidebar)
|
||||||
--screenshot=path.bmp capture at quiescence and quit
|
--screenshot=path.bmp capture at quiescence and quit
|
||||||
--width=W --height=H (default 1000 / 700)
|
--width=W --height=H (default 1000 / 700)
|
||||||
@@ -25,15 +25,14 @@ internal data class CliArgs(
|
|||||||
val maxFrames: Int = 300,
|
val maxFrames: Int = 300,
|
||||||
)
|
)
|
||||||
|
|
||||||
/** Translates the --gpu= string to the right GpuMode sealed instance.
|
/** Translates the --gpu= string to a GpuMode. Accepts the bare driver names
|
||||||
Accepts both dotted (`skia.metal`) and dashed (`skia-metal`) forms,
|
(`metal`, `opengl`, `cpu`) plus the retired `skia.*` / `skia-*` spellings, so
|
||||||
plus the bare driver names (`metal`, `opengl`, …) as Skia aliases
|
existing scripts and docs keep working after GpuMode was flattened. */
|
||||||
for backwards compatibility. */
|
|
||||||
private fun parseGpu(inValue: String): GpuMode = when (inValue.lowercase().replace('-', '.')) {
|
private fun parseGpu(inValue: String): GpuMode = when (inValue.lowercase().replace('-', '.')) {
|
||||||
"auto" -> GpuMode.Auto
|
"auto" -> GpuMode.Auto
|
||||||
"none", "cpu", "software" -> GpuMode.Software
|
"none", "cpu", "software", "raster" -> GpuMode.CpuRaster
|
||||||
"metal", "skia.metal" -> GpuMode.Skia.Metal
|
"metal", "skia.metal" -> GpuMode.Metal
|
||||||
"opengl", "gl", "skia.opengl" -> GpuMode.Skia.OpenGL
|
"opengl", "gl", "skia.opengl" -> GpuMode.OpenGL
|
||||||
else -> {
|
else -> {
|
||||||
println("Unknown --gpu=$inValue, using auto")
|
println("Unknown --gpu=$inValue, using auto")
|
||||||
GpuMode.Auto
|
GpuMode.Auto
|
||||||
|
|||||||
+1
-1
@@ -278,7 +278,7 @@ internal object IconCodec {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** An 8-byte little-endian [width][height] header + straight-alpha RGBA - the
|
/** An 8-byte little-endian [width][height] header + straight-alpha RGBA - the
|
||||||
format the runtime (WindowIcon.kt / SDL3Backend) reads. */
|
format the runtime (WindowIcon.kt / Sdl3Backend) reads. */
|
||||||
fun rgbaBlob(inW: Int, inH: Int, inRgba: ByteArray): ByteArray {
|
fun rgbaBlob(inW: Int, inH: Int, inRgba: ByteArray): ByteArray {
|
||||||
val vOut = ByteArray(8 + inRgba.size)
|
val vOut = ByteArray(8 + inRgba.size)
|
||||||
le32(vOut, 0, inW)
|
le32(vOut, 0, inW)
|
||||||
|
|||||||
+1
-1
@@ -20,7 +20,7 @@ import org.gradle.api.tasks.Copy
|
|||||||
//
|
//
|
||||||
// Text rendering goes through Skia's skparagraph (HarfBuzz + skunicode). On
|
// Text rendering goes through Skia's skparagraph (HarfBuzz + skunicode). On
|
||||||
// Windows, Skia loads its ICU data (icudtl.dat) from a file next to the binary at
|
// Windows, Skia loads its ICU data (icudtl.dat) from a file next to the binary at
|
||||||
// runtime - without it ParagraphBuilder fatal-aborts (check(fUnicode)). Unlike
|
// runtime - without it ParagraphBuilder fatal-aborts (check(mUnicode)). Unlike
|
||||||
// macOS/Linux skiko, which bake the ICU data in, the Windows build ships it as a
|
// macOS/Linux skiko, which bake the ICU data in, the Windows build ships it as a
|
||||||
// sidecar. The official skiko-awt-runtime-windows-x64 jar already contains a
|
// sidecar. The official skiko-awt-runtime-windows-x64 jar already contains a
|
||||||
// compatible icudtl.dat (same Skia base as the fork), so we source it from there
|
// compatible icudtl.dat (same Skia base as the fork), so we source it from there
|
||||||
|
|||||||
Reference in New Issue
Block a user