[JEWEL-1076, IJPL-176416] New Icons API#3415
[JEWEL-1076, IJPL-176416] New Icons API#3415jakub-senohrabek wants to merge 9 commits intomasterfrom
Conversation
|
We do not have configured codex bot here to review, right ;)? |
3aa479e to
23ccf16
Compare
| <dependencies> | ||
| <!-- region Generated dependencies - run `Generate Product Layouts` to regenerate --> | ||
| <module name="intellij.libraries.kotlinx.serialization.core"/> | ||
| <module name="intellij.platform.icons.api"/> |
There was a problem hiding this comment.
Do we expect that every plugin requiring it will need to add explicit dependency? I believe it must be exposed transitively from some platform modules
| /** | ||
| * Layer for embedding swing icons. | ||
| */ | ||
| @ExperimentalIconsApi |
There was a problem hiding this comment.
Does it require constant opt-ins in every usage? May be plain @Experimental instead?
|
Why does this change have status DRAFT? Is it ready for review? |
23ccf16 to
efbd0c1
Compare
GitOrigin-RevId: 820ec33e12e0e36356344d66c7a7b4720f70e31c
…ce loaders GitOrigin-RevId: be0d90ab20c1893c16e1ab3a0f408ce1a962da89
GitOrigin-RevId: e1993b24c61995c2b933554dab44cec34a484733
GitOrigin-RevId: eb7961d74037d22dfe18266924710002c2c60c8c
GitOrigin-RevId: 91466a710fc92ab6ad80b3667cc3600a72601268
…ion modules GitOrigin-RevId: 21a95c9085658ac565ac8c23bc1b3b0748e8dc15
GitOrigin-RevId: b36c12cfd47a204d2af7352d9d0db849164a65f4
GitOrigin-RevId: 51208a5aaaf467cd4d9b43d2d9ab1b2d8147d110
…ependency GitOrigin-RevId: 5b0162d5dca56987bbb014ff7f34a78b4bb9582b
efbd0c1 to
8a68150
Compare
rock3r
left a comment
There was a problem hiding this comment.
As we talked about offline, I’m adding this here mostly to document that conversation in the review.
A broader architectural concern I still have is how this is supposed to work cleanly in Jewel standalone, which is a real target for this API and not optional.
Right now it still feels like some important parts of icon resource resolution are effectively trapped inside the IntelliJ Platform icon-loading stack (IconLoader / util-ui/icons), especially around path transformation / mapping / variant selection. That makes me nervous, because I don’t think we want either of these outcomes:
- Jewel standalone depending directly on the full IJPL icon-loading implementation
- or duplicating the same path-mapping logic in a separate standalone stack
Given that standalone support is a hard requirement, would it make sense to explicitly extract the resource-path resolution part into a smaller backend-neutral module that both IJPL and Jewel standalone can depend on?
By “path resolution” I mean the logic that takes a logical icon path and environment/context and produces the effective resource candidates / transformations (e.g. theme/UI variants, size-specialized variants, any other canonical path mapping rules). The actual loading/caching/rendering backends can still remain platform-specific, but the mapping rules themselves seem like they want to be shared.
Otherwise I worry we end up with an awkward split where:
- the icon model is portable
- the rendering backends are separate
- but the resource resolution semantics are still IntelliJ-specific and therefore either unavailable or reimplemented differently in standalone
That feels like exactly the sort of thing that will cause subtle divergence over time. Since standalone support is non-negotiable here, extracting that path-resolution layer seems like the cleaner long-term shape.
And to be explicit: I do think this is part of the implementation work required for this feature, and it will need to be addressed in this PR before merge.
|
|
||
| override suspend fun resolve(): Icon { | ||
| val resolved = resolvedIcon | ||
| if (resolved != null) return resolved |
There was a problem hiding this comment.
I think this can break:
• InPlaceDeferredIconResolver.resolve() returns immediately when resolvedIcon != null
• But DefaultDeferredIconRenderer only flips from placeholder/null to the resolved renderer in whenDone()
• New renderers start with isDone = false, renderer = icon.placeholder?.createRenderer(...)
So if a renderer is created after the deferred icon was already resolved, it never gets whenDone(), and resolve() now short-circuits without re-notifying listeners. That means the new renderer can stay stuck on the placeholder/ null forever.
There’s a second broken path here too:
• DeferredIconResolverService.getOrCreateDeferredIcon() returns resolver?.deferredIcon?.get() ?: DefaultDeferredIcon(identifier, placeholder)
• If the resolver is still in the map but its weak ref was cleared, callers get a fresh DefaultDeferredIcon that is not connected to the existing resolver state
| icon: Icon, | ||
| contentDescription: String?, | ||
| modifier: Modifier = Modifier, | ||
| loadingStrategy: LoadingStrategy = LoadingStrategy.BlockThread |
There was a problem hiding this comment.
This seems unused, is it intended?
| @InternalJewelApi | ||
| @ApiStatus.Internal | ||
| public class RendererBasedIconPainter(private val iconRenderer: IconRenderer, private val scaling: ScalingContext) : Painter() { | ||
| override val intrinsicSize: Size = iconRenderer.calculateExpectedDimensions(scaling).toComposeSize() |
There was a problem hiding this comment.
This is only computed once, is it intended? I would imagine this could break with animated or deferred icons if the dimensions change (e.g., the placeholder has a different size from the final icon)
| val icon: Icon | ||
| ): javax.swing.Icon { | ||
| private val renderer by lazy { | ||
| IconRendererManager.getInstance().createRenderer(icon, RenderingContext.Empty) // TODO listen for updates to redraw Icon |
There was a problem hiding this comment.
Just noting — this TODO will need to be addressed, or it'll break rendering animated/deferred icons in Swing
| override fun paintIcon(c: Component?, g: Graphics, x: Int, y: Int) { | ||
| val scaling = getScaling(g) | ||
| val boundsSize = if (c != null) { | ||
| Dimensions(c.width, c.height) |
There was a problem hiding this comment.
That’s not how javax.swing.Icon usually behaves. A Swing icon should paint into its own icon bounds, not expand to the host component’s full size. In things like buttons/labels with text, this risks stretching the icon to the component size instead of respecting getIconWidth()/getIconHeight().
Local repro from the playground: this is the same row(spacing = 25.percent) { ... } icon rendered through toSwingIcon() under two different host sizes.
What’s striking is that the icon’s internal layout changes based on the available host width: instead of behaving like a small icon composed of two children plus spacing, it gets horizontally redistributed/squeezed as the host gets wider. That strongly suggests the renderer is resolving layout against the surrounding component bounds rather than a stable icon canvas / getIconWidth() × getIconHeight().
So this does not just look theoretically “non-Swing-like” — it produces visibly wrong results for bounds-relative/layout-based icons in practice.
There was a problem hiding this comment.
This can produce negative width / height when the margins exceed the available parent bounds.
Do we have a defined contract for that? If negative bounds are allowed to leak further down the pipeline, Compose can crash on them during draw. It seems like this path should either clamp to zero or validate/reject impossible layouts earlier.
There was a problem hiding this comment.
I think this may be missing an update/repaint notification.
When the wrapped legacy DeferredIcon finishes, this path updates its cached image, but I don't see it notifying the new icon renderer/frontend via renderingContext.updateFlow.triggerUpdate() (or equivalent). If so, the visual update would depend on some unrelated repaint happening later.
That seems especially risky for the migration/interop path, because javax.swing.Icon.toNewIcon() can wrap exactly these old deferred icons.
There was a problem hiding this comment.
I don't think x / y should be affecting the backing-layer dimensions here.
In Swing, paintIcon(c, g, x, y) uses x/y as the paint offset, not as a reduction in the icon's available size. Painting the same icon at (0, 0) vs (10, 10) should translate it, not shrink/crop the backing buffer by 10 pixels in each dimension.
As written, this seems likely to produce clipped/incorrect output for icons painted at non-zero coordinates.
There was a problem hiding this comment.
Is this a bug? The extension point says custom providers are supposed to implement createRenderer(...), but this path checks extension.handles(layer) and then unconditionally returns SwingIconLayerRenderer(...) instead of delegating to extension.createRenderer(...).
If I am reading this correctly, custom renderer providers can never actually provide their own renderer implementation here.
There was a problem hiding this comment.
spacing seems to be missing from both equals() and hashCode() here.
That means two layout layers that differ only by spacing compare equal, even though spacing materially affects rendering. Given how much this new icon model seems to rely on value-like model objects, caching/reuse, and caller-side remembering, this looks like a real correctness bug rather than just an implementation detail.
|
I think this PR still needs much stronger test coverage before merge. The new icon system is no longer just "paint an image"; it is now a compositional tree with layout, sizing, modifiers, deferred resolution, animation, and toolkit-specific update/repaint behavior. That means a lot of correctness now lives in the alignment between measurement and rendering, and that is exactly where I would expect subtle regressions. I only found a very basic smoke test that renders a new icon to a
Even if some of these behaviors are intentionally unsupported, I think that contract still needs to be made explicit and then locked down with tests. Right now the absence of coverage here makes me pretty nervous, because these are exactly the kinds of issues that tend to look fine in simple demos and then break in real UI usage. My strong suggestion would be:
|
|
PS I have a few tweaks/samples to propose that I referred to in the review public/jakub.senohrabek/new-icons-api...rock3r:sebp/new-icons-tweaks |
This PR adds new icons api that allows cross-api icons (compose, swing), and icons serialization. The implementation is separated from the API (and the rendering part from data part) to allow creation of Icons without having to depend on UI code. (useful when creating icons on backend)
Make sure to start with intellij.platform.icons.api module, which contains the core api. (also check the rendering submodule). Afterwards, check the impl module (start with intellij submodule) and jewel implementation. (changed jewel modules)
GitOrigin-RevId: 32b987b2a7bba316208aefd998a523f6a74ace30