mirror of
https://github.com/bitsycore/compose-desktop-native.git
synced 2026-10-05 10:47:26 +00:00
Compare commits
4
Commits
10380e5422
...
0c09109e6e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c09109e6e | ||
|
|
5804128e1d | ||
|
|
04d1df58de | ||
|
|
a87ff2d7f6 |
@@ -813,6 +813,18 @@ Two residuals, neither a text-metrics bug:
|
|||||||
(0.29% at +0px vs 2.13% at +1px), where before +2px halved the error. The residual 0.29%
|
(0.29% at +0px vs 2.13% at +1px), where before +2px halved the error. The residual 0.29%
|
||||||
is rasterizer AA.
|
is rasterizer AA.
|
||||||
|
|
||||||
|
- **Icon-font glyphs rendered LOW - ✅ FIXED.** Material Symbols sat 2-4px lower on native
|
||||||
|
than on jvm, the offset scaling with icon size (~13% of it). Cause: the JVM actual centres
|
||||||
|
the glyph explicitly (`y = height/2 - (ascent+descent)/2`), while `IconFontIcon` placed
|
||||||
|
`IconText` in a Box at the DEFAULT TopStart alignment - and the glyph's natural text box is
|
||||||
|
taller than the nominal icon size. Fixed by centring, with
|
||||||
|
`wrapContentSize(unbounded = true)` so the text measures at its natural size first:
|
||||||
|
`Modifier.size()` imposes FIXED constraints, so without it nothing overflows and
|
||||||
|
contentAlignment is a no-op (a first attempt without it changed literally nothing).
|
||||||
|
gear/download/lock are now pixel-exact; the eye is 1px off from half-pixel rounding (odd
|
||||||
|
glyph height centred in an even box; jvm draws at a float y, native placement is integer).
|
||||||
|
Whole-window >32/255 drift 0.29% -> 0.17%.
|
||||||
|
|
||||||
- **Missing glyph in the session header.** The leading icon next to "Untitled session"
|
- **Missing glyph in the session header.** The leading icon next to "Untitled session"
|
||||||
renders as a `.notdef` TOFU on jvm and as NOTHING on native - so the glyph is absent from
|
renders as a `.notdef` TOFU on jvm and as NOTHING on native - so the glyph is absent from
|
||||||
the subsetted Material Symbols font on both, and the two stacks just disagree on how to
|
the subsetted Material Symbols font on both, and the two stacks just disagree on how to
|
||||||
|
|||||||
+15
-1
@@ -2,7 +2,9 @@ package com.compose.sdl.icons
|
|||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.wrapContentSize
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.font.FontVariation
|
import androidx.compose.ui.text.font.FontVariation
|
||||||
@@ -36,9 +38,21 @@ fun IconFontIcon(
|
|||||||
size: Dp = IconDefaults.DefaultIconSize,
|
size: Dp = IconDefaults.DefaultIconSize,
|
||||||
fontVariationSettings: List<FontVariation.Setting>? = null,
|
fontVariationSettings: List<FontVariation.Setting>? = null,
|
||||||
) {
|
) {
|
||||||
Box(modifier = modifier.size(size)) {
|
// CENTER the glyph in the icon box. The glyph's natural text box is TALLER than
|
||||||
|
// `size` - its height is the font's ascent+descent, and Material Symbols' em box
|
||||||
|
// overshoots the nominal icon size - so the glyph used to render ~13% of the icon
|
||||||
|
// size too LOW. This mirrors the JVM actual's explicit baseline math
|
||||||
|
// (`y = height/2 - (ascent+descent)/2` in MaterialSymbolsIcon.jvm.kt): IconText
|
||||||
|
// sets no lineHeight, so its box height IS ascent+descent and centering that box
|
||||||
|
// centers the same span the JVM centers.
|
||||||
|
//
|
||||||
|
// `wrapContentSize(unbounded = true)` is load-bearing: Modifier.size() hands the
|
||||||
|
// child FIXED constraints, so without it the text is measured at exactly `size`,
|
||||||
|
// nothing overflows, and contentAlignment has nothing to align.
|
||||||
|
Box(modifier = modifier.size(size), contentAlignment = Alignment.Center) {
|
||||||
com.compose.sdl.text.IconText(
|
com.compose.sdl.text.IconText(
|
||||||
text = codepointToString(codepoint),
|
text = codepointToString(codepoint),
|
||||||
|
modifier = Modifier.wrapContentSize(Alignment.Center, unbounded = true),
|
||||||
fontFamily = fontFamily,
|
fontFamily = fontFamily,
|
||||||
color = tint,
|
color = tint,
|
||||||
fontSize = size.value.sp,
|
fontSize = size.value.sp,
|
||||||
|
|||||||
+18
-1
@@ -10,6 +10,7 @@ import org.gradle.api.tasks.OutputDirectory
|
|||||||
import org.gradle.api.tasks.OutputFile
|
import org.gradle.api.tasks.OutputFile
|
||||||
import org.gradle.api.tasks.PathSensitive
|
import org.gradle.api.tasks.PathSensitive
|
||||||
import org.gradle.api.tasks.PathSensitivity
|
import org.gradle.api.tasks.PathSensitivity
|
||||||
|
import org.gradle.work.DisableCachingByDefault
|
||||||
import org.gradle.api.tasks.TaskAction
|
import org.gradle.api.tasks.TaskAction
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.zip.Inflater
|
import java.util.zip.Inflater
|
||||||
@@ -91,10 +92,23 @@ internal fun installAppIcon(inProject: Project, inExt: ComposeDesktopNativeExten
|
|||||||
}
|
}
|
||||||
// Link the object last: the mingw link tasks must run windres first (the
|
// Link the object last: the mingw link tasks must run windres first (the
|
||||||
// path is referenced via linkerOpts, added reflectively in NativeApplication).
|
// path is referenced via linkerOpts, added reflectively in NativeApplication).
|
||||||
|
//
|
||||||
|
// `inputs.files(task)` rather than a bare `dependsOn`: dependsOn only orders
|
||||||
|
// the two, it does NOT make the object's CONTENT part of the link's up-to-date
|
||||||
|
// check. The .o path reaches the linker as a linkerOpts STRING, which is an
|
||||||
|
// input only as a string - so editing an icon PNG rebuilt the .ico and the .o
|
||||||
|
// and then left the link UP-TO-DATE, silently shipping an .exe with the OLD
|
||||||
|
// icon embedded. Declaring the task's outputs as link inputs carries the
|
||||||
|
// dependency AND invalidates on content.
|
||||||
inProject.tasks.matching {
|
inProject.tasks.matching {
|
||||||
it.name == "linkDebugExecutableMingwX64" || it.name == "linkReleaseExecutableMingwX64" ||
|
it.name == "linkDebugExecutableMingwX64" || it.name == "linkReleaseExecutableMingwX64" ||
|
||||||
it.name == "runDebugExecutableMingwX64" || it.name == "runReleaseExecutableMingwX64"
|
it.name == "runDebugExecutableMingwX64" || it.name == "runReleaseExecutableMingwX64"
|
||||||
}.configureEach { it.dependsOn(vWindresTask) }
|
}.configureEach { task ->
|
||||||
|
task.inputs.files(vWindresTask)
|
||||||
|
.withPropertyName("composeNativeAppIconObject")
|
||||||
|
.withPathSensitivity(PathSensitivity.NONE)
|
||||||
|
task.dependsOn(vWindresTask)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,6 +116,7 @@ internal fun installAppIcon(inProject: Project, inExt: ComposeDesktopNativeExten
|
|||||||
// MARK: Tasks
|
// MARK: Tasks
|
||||||
// ==================
|
// ==================
|
||||||
|
|
||||||
|
@DisableCachingByDefault(because = "trivial PNG decode; caching would cost more than it saves")
|
||||||
abstract class GenerateRgbaIconsTask : DefaultTask() {
|
abstract class GenerateRgbaIconsTask : DefaultTask() {
|
||||||
@get:InputFiles
|
@get:InputFiles
|
||||||
@get:PathSensitive(PathSensitivity.NONE)
|
@get:PathSensitive(PathSensitivity.NONE)
|
||||||
@@ -124,6 +139,7 @@ abstract class GenerateRgbaIconsTask : DefaultTask() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@DisableCachingByDefault(because = "trivial byte concatenation of already-local PNGs")
|
||||||
abstract class GenerateIcoTask : DefaultTask() {
|
abstract class GenerateIcoTask : DefaultTask() {
|
||||||
@get:InputFiles
|
@get:InputFiles
|
||||||
@get:PathSensitive(PathSensitivity.NONE)
|
@get:PathSensitive(PathSensitivity.NONE)
|
||||||
@@ -140,6 +156,7 @@ abstract class GenerateIcoTask : DefaultTask() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@DisableCachingByDefault(because = "shells out to the host windres; not portable across machines")
|
||||||
abstract class CompileWindowsIconResourceTask : DefaultTask() {
|
abstract class CompileWindowsIconResourceTask : DefaultTask() {
|
||||||
@get:InputFiles
|
@get:InputFiles
|
||||||
@get:PathSensitive(PathSensitivity.NONE)
|
@get:PathSensitive(PathSensitivity.NONE)
|
||||||
|
|||||||
+32
-11
@@ -88,18 +88,39 @@ private fun registerDataKresTasks(project: Project) {
|
|||||||
val compress = project.providers.gradleProperty("compressResources").orNull?.toBoolean() ?: false
|
val compress = project.providers.gradleProperty("compressResources").orNull?.toBoolean() ?: false
|
||||||
task.entryCompression = if (compress) ZipEntryCompression.DEFLATED else ZipEntryCompression.STORED
|
task.entryCompression = if (compress) ZipEntryCompression.DEFLATED else ZipEntryCompression.STORED
|
||||||
task.duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
task.duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||||
val resPackage = project.resolveResourcePackage()
|
// MULTI-MODULE: source from Kotlin's per-target resource AGGREGATE, which
|
||||||
val prepareNames = sourceSets
|
// already merges this project's OWN composeResources with those of every
|
||||||
.map { "prepareComposeResourcesTaskFor" + it.replaceFirstChar { c -> c.uppercase() } }
|
// dependency module (each under its own `composeResources/<package>/`).
|
||||||
.filter { it in project.tasks.names }
|
// Reading the project's `preparedResources/` instead - as this did - silently
|
||||||
for (prepareName in prepareNames) {
|
// dropped every library module's resources from data.kres, and forced a
|
||||||
val sourceSet = prepareName.removePrefix("prepareComposeResourcesTaskFor")
|
// single `resPackage` onto the lot, which is wrong the moment more than one
|
||||||
.replaceFirstChar { it.lowercase() }
|
// module contributes.
|
||||||
val prepared = project.layout.buildDirectory.dir(
|
//
|
||||||
"generated/compose/resourceGenerator/preparedResources/$sourceSet/composeResources"
|
// The aggregate directory is already rooted at `composeResources/`, so it is
|
||||||
|
// copied in with NO `into()` remapping.
|
||||||
|
val aggregateName = "${target.replaceFirstChar { it.lowercase() }}AggregateResources"
|
||||||
|
if (aggregateName in project.tasks.names) {
|
||||||
|
val aggregated = project.layout.buildDirectory.dir(
|
||||||
|
"kotlin-multiplatform-resources/aggregated-resources/${target.replaceFirstChar { it.lowercase() }}"
|
||||||
)
|
)
|
||||||
task.from(prepared) { spec -> spec.into("composeResources/$resPackage") }
|
task.from(aggregated)
|
||||||
task.dependsOn(project.tasks.named(prepareName))
|
task.dependsOn(project.tasks.named(aggregateName))
|
||||||
|
} else {
|
||||||
|
// Fallback for Kotlin versions without the resources-aggregation tasks:
|
||||||
|
// this project's own resources only (the pre-existing behaviour).
|
||||||
|
val resPackage = project.resolveResourcePackage()
|
||||||
|
val prepareNames = sourceSets
|
||||||
|
.map { "prepareComposeResourcesTaskFor" + it.replaceFirstChar { c -> c.uppercase() } }
|
||||||
|
.filter { it in project.tasks.names }
|
||||||
|
for (prepareName in prepareNames) {
|
||||||
|
val sourceSet = prepareName.removePrefix("prepareComposeResourcesTaskFor")
|
||||||
|
.replaceFirstChar { it.lowercase() }
|
||||||
|
val prepared = project.layout.buildDirectory.dir(
|
||||||
|
"generated/compose/resourceGenerator/preparedResources/$sourceSet/composeResources"
|
||||||
|
)
|
||||||
|
task.from(prepared) { spec -> spec.into("composeResources/$resPackage") }
|
||||||
|
task.dependsOn(project.tasks.named(prepareName))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
project.tasks.matching { it.name == linkName }.configureEach { it.dependsOn(zipTask) }
|
project.tasks.matching { it.name == linkName }.configureEach { it.dependsOn(zipTask) }
|
||||||
|
|||||||
Reference in New Issue
Block a user