feat(jetbrains): add Align wrapper with TRACK mode, replace CenterShrinkPanel

This commit is contained in:
kirillk
2026-05-22 13:37:55 -04:00
parent fa4bc15117
commit dcfa1029c3
5 changed files with 591 additions and 42 deletions
+38
View File
@@ -366,6 +366,44 @@ For common spacing lookups, prefer `JBUI.CurrentTheme` area-specific insets (e.g
| Simple `BorderLayout` panels | `JBUI.Panels.simplePanel(...)`, `BorderLayoutPanel` |
| Simple vertical custom Swing groups | `VerticalLayout` |
| Fluent platform panels | `JBPanel.withBorder(...)`, `.andTransparent()`, `.andOpaque()`, `.withBackground(...)` |
| Single-component alignment wrapper | `ai.kilocode.client.ui.Align` — see section below |
### Align — Single-Component Alignment Wrapper
Use `Align` (`ai.kilocode.client.ui.Align`) when a single Swing component must be positioned inside available space without adding visual chrome. It is a transparent, no-border, no-color `JPanel(null)` that lays out its one child according to independent horizontal (`HAlign`) and vertical (`VAlign`) modes. `CenterShrinkPanel` has been removed; use `child.alignCenter()` as a direct replacement.
**Alignment modes:**
| Mode | Axis | Layout behavior | Wrapper size contribution |
|---|---|---|---|
| `HAlign.TRACK` / `VAlign.TRACK` | either | Child always fills all available space; ignores child min/preferred/max | Zero (wrapper reports insets only on that axis) |
| `HAlign.FIT` / `VAlign.FIT` | either | Child fills available space clamped to child's effective `[min, max]` range | Child min/preferred/max respected |
| `HAlign.LEFT` / `VAlign.TOP` | H / V | Child placed at left/top edge at bounded preferred size; shrinks to available when necessary | Child min/preferred/max respected |
| `HAlign.CENTER` / `VAlign.CENTER` | H / V | Child centered at bounded preferred size; shrinks to available when necessary | Child min/preferred/max respected |
| `HAlign.RIGHT` / `VAlign.BOTTOM` | H / V | Child placed at right/bottom edge at bounded preferred size; shrinks to available when necessary | Child min/preferred/max respected |
"Bounded preferred" means the child's preferred size coerced into the effective `[min, max]` range. If available space is smaller than the effective minimum, the layout shrinks the child to available space to avoid overflow.
**Kotlin-style factory extensions** on `Component`:
```kotlin
child.align(HAlign.LEFT, VAlign.TOP) // explicit modes
child.alignCenter() // CENTER / CENTER (replaces CenterShrinkPanel)
child.alignLeft(VAlign.CENTER) // LEFT + custom V
child.alignRight(VAlign.CENTER) // RIGHT + custom V
child.alignTop(HAlign.CENTER) // TOP + custom H
child.alignBottom() // BOTTOM + FIT horizontal
child.track() // TRACK / TRACK — always fills all space
child.trackX(VAlign.TOP) // TRACK horizontal, TOP vertical
child.trackY(HAlign.CENTER) // CENTER horizontal, TRACK vertical
```
**Rules:**
- Prefer the factory extensions over creating one-off `JPanel(FlowLayout(...))` or `BorderLayoutPanel` wrappers just to control alignment.
- Use `TRACK` when the child must occupy all available space on an axis and must not reserve any space in the parent's size negotiation on that axis. Use `FIT` when you want to fill available space but still respect child min/max constraints.
- All non-TRACK modes include the child's min, preferred, and max sizes in the wrapper's own min/preferred/max size. This means parent layout managers see the child constraints through the wrapper.
- Do not use `Align` for spacing, padding, borders, colors, or multi-child layout — use `JBUI.Borders.empty(...)`, `UiStyle.Gap`, or an appropriate layout manager for those concerns.
### IntelliJ UI Surfaces
@@ -10,8 +10,9 @@ import ai.kilocode.client.session.ui.style.SessionEditorStyle
import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget
import ai.kilocode.client.session.ui.style.SessionUiStyle
import ai.kilocode.client.session.controller.SessionController
import ai.kilocode.client.ui.CenterShrinkPanel
import ai.kilocode.client.ui.Align
import ai.kilocode.client.ui.UiStyle
import ai.kilocode.client.ui.alignCenter
import ai.kilocode.rpc.dto.SessionDto
import com.intellij.icons.AllIcons
import com.intellij.openapi.Disposable
@@ -44,7 +45,7 @@ import javax.swing.ListSelectionModel
* Empty-session panel.
*
* The content is a BorderLayout panel, wrapped in a
* [CenterShrinkPanel] (exposed as [view]) so callers need not know about centering.
* [Align] (exposed as [view]) so callers need not know about centering.
*/
class EmptySessionPanel(
parent: Disposable,
@@ -52,7 +53,7 @@ class EmptySessionPanel(
recents: List<SessionDto>,
private val history: () -> Unit = {},
) : BorderLayoutPanel(), Disposable, SessionEditorStyleTarget {
val view: CenterShrinkPanel = CenterShrinkPanel(this)
val view: Align = alignCenter()
private val model = DefaultListModel<LocalHistoryItem>()
private var hover = -1
@@ -133,7 +134,7 @@ class EmptySessionPanel(
val header = BorderLayoutPanel(0, gap).apply {
isOpaque = false
add(logo, BorderLayout.NORTH)
add(CenterShrinkPanel(description), BorderLayout.CENTER)
add(description.alignCenter(), BorderLayout.CENTER)
}
val recent = BorderLayoutPanel().apply {
@@ -0,0 +1,152 @@
package ai.kilocode.client.ui
import java.awt.Component
import java.awt.Dimension
import javax.swing.JPanel
enum class HAlign { TRACK, FIT, LEFT, CENTER, RIGHT }
enum class VAlign { TRACK, FIT, TOP, CENTER, BOTTOM }
/**
* A transparent wrapper panel that positions its single child according to independent
* horizontal ([h]) and vertical ([v]) alignment modes.
*
* **TRACK**: child fills all available space on that axis, ignoring child min/preferred/max.
* The wrapper reports zero contribution from the child on that axis for its own min/preferred/max.
*
* **FIT**: child fills available space clamped to child's effective [min, max] range.
*
* **LEFT / CENTER / RIGHT** (horizontal) and **TOP / CENTER / BOTTOM** (vertical):
* child uses its bounded preferred size (coerced into [min, max]) and is placed at the
* corresponding edge or centered. Shrinks to available space when necessary.
*
* Wrapper min/preferred/max sizes are computed by combining the per-axis child contribution
* (zero for TRACK axes) with the panel insets.
*
* Use the Kotlin-style factory extensions for concise call sites:
* ```
* label.alignCenter()
* button.alignRight(VAlign.CENTER)
* panel.align(HAlign.LEFT, VAlign.TOP)
* content.track()
* scrollable.trackX(VAlign.TOP)
* ```
*/
class Align(
child: Component,
private val h: HAlign = HAlign.FIT,
private val v: VAlign = VAlign.FIT,
) : JPanel(null) {
init {
isOpaque = false
add(child)
}
// -----------------------------------------------------------------------
// Layout
// -----------------------------------------------------------------------
override fun doLayout() {
if (componentCount == 0) return
val child = getComponent(0)
val ins = insets
val availW = maxOf(0, width - ins.left - ins.right)
val availH = maxOf(0, height - ins.top - ins.bottom)
val (w, cx) = placeAxis(h, availW, child.minimumSize.width, child.preferredSize.width, child.maximumSize.width)
val (ht, cy) = placeAxis(v, availH, child.minimumSize.height, child.preferredSize.height, child.maximumSize.height)
child.setBounds(ins.left + cx, ins.top + cy, w, ht)
}
// -----------------------------------------------------------------------
// Wrapper size negotiation
// -----------------------------------------------------------------------
override fun getMinimumSize(): Dimension {
if (componentCount == 0) return super.getMinimumSize()
val child = getComponent(0)
val ins = insets
val cw = if (h == HAlign.TRACK) 0 else child.minimumSize.width
val ch = if (v == VAlign.TRACK) 0 else child.minimumSize.height
return Dimension(cw + ins.left + ins.right, ch + ins.top + ins.bottom)
}
override fun getPreferredSize(): Dimension {
if (componentCount == 0) return super.getPreferredSize()
val child = getComponent(0)
val ins = insets
val cw = if (h == HAlign.TRACK) 0 else bounded(child.preferredSize.width, child.minimumSize.width, child.maximumSize.width)
val ch = if (v == VAlign.TRACK) 0 else bounded(child.preferredSize.height, child.minimumSize.height, child.maximumSize.height)
return Dimension(cw + ins.left + ins.right, ch + ins.top + ins.bottom)
}
override fun getMaximumSize(): Dimension {
if (componentCount == 0) return super.getMaximumSize()
val child = getComponent(0)
val ins = insets
val cw = if (h == HAlign.TRACK) super.getMaximumSize().width else maxOf(child.minimumSize.width, child.maximumSize.width) + ins.left + ins.right
val ch = if (v == VAlign.TRACK) super.getMaximumSize().height else maxOf(child.minimumSize.height, child.maximumSize.height) + ins.top + ins.bottom
return Dimension(cw, ch)
}
}
// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------
/**
* Returns (size, offset) for a single axis. Offset is relative to the inner origin (after insets).
* - TRACK: size = avail, offset = 0
* - FIT: size = clamp(avail, min, max), offset = 0
* - edge/center: size = clamp(boundedPref, 0, avail), offset positions according to alignment
*/
private fun placeAxis(mode: Any, avail: Int, min: Int, pref: Int, max: Int): Pair<Int, Int> {
val effMax = maxOf(min, max)
return when (mode) {
HAlign.TRACK, VAlign.TRACK -> avail to 0
HAlign.FIT, VAlign.FIT -> {
// fill available, capped at effMax; if avail < min we still shrink to avail
val size = minOf(avail, effMax)
size to 0
}
HAlign.LEFT, VAlign.TOP -> {
val size = minOf(bounded(pref, min, effMax), avail)
size to 0
}
HAlign.CENTER, VAlign.CENTER -> {
val size = minOf(bounded(pref, min, effMax), avail)
size to (avail - size) / 2
}
HAlign.RIGHT, VAlign.BOTTOM -> {
val size = minOf(bounded(pref, min, effMax), avail)
size to (avail - size)
}
else -> avail to 0
}
}
private fun bounded(value: Int, min: Int, max: Int) = value.coerceIn(min, maxOf(min, max))
// ---------------------------------------------------------------------------
// Kotlin-style factory extensions
// ---------------------------------------------------------------------------
fun Component.align(h: HAlign = HAlign.FIT, v: VAlign = VAlign.FIT) = Align(this, h, v)
fun Component.alignCenter() = Align(this, HAlign.CENTER, VAlign.CENTER)
fun Component.alignLeft(v: VAlign = VAlign.FIT) = Align(this, HAlign.LEFT, v)
fun Component.alignRight(v: VAlign = VAlign.FIT) = Align(this, HAlign.RIGHT, v)
fun Component.alignTop(h: HAlign = HAlign.FIT) = Align(this, h, VAlign.TOP)
fun Component.alignBottom(h: HAlign = HAlign.FIT) = Align(this, h, VAlign.BOTTOM)
fun Component.track() = Align(this, HAlign.TRACK, VAlign.TRACK)
fun Component.trackX(v: VAlign = VAlign.FIT) = Align(this, HAlign.TRACK, v)
fun Component.trackY(h: HAlign = HAlign.FIT) = Align(this, h, VAlign.TRACK)
@@ -1,38 +0,0 @@
package ai.kilocode.client.ui
import java.awt.Component
import java.awt.Dimension
import javax.swing.JPanel
/**
* Centers its single child and shrinks it to available space when needed.
* If available space is larger than the child's maximum size, the child is not expanded.
*/
class CenterShrinkPanel(child: Component) : JPanel(null) {
init {
isOpaque = false
add(child)
}
override fun doLayout() {
if (componentCount == 0) return
val child = getComponent(0)
val insets = getInsets()
val availW = width - insets.left - insets.right
val availH = height - insets.top - insets.bottom
val pref = child.preferredSize
val max = child.maximumSize
val w = minOf(pref.width, max.width, availW)
val h = minOf(pref.height, max.height, availH)
val x = insets.left + (availW - w) / 2
val y = insets.top + (availH - h) / 2
child.setBounds(x, y, w, h)
}
override fun getPreferredSize(): Dimension {
if (componentCount == 0) return super.getPreferredSize()
val pref = getComponent(0).preferredSize
val insets = getInsets()
return Dimension(pref.width + insets.left + insets.right, pref.height + insets.top + insets.bottom)
}
}
@@ -0,0 +1,396 @@
package ai.kilocode.client.ui
import com.intellij.testFramework.fixtures.BasePlatformTestCase
import com.intellij.ui.components.JBLabel
import com.intellij.util.ui.JBUI
import java.awt.Dimension
@Suppress("UnstableApiUsage")
class AlignTest : BasePlatformTestCase() {
// ------ structure ------
fun `test wrapper is non-opaque`() {
assertFalse(Align(JBLabel("x"), HAlign.FIT, VAlign.FIT).isOpaque)
}
fun `test wrapper contains exactly the wrapped child`() {
val child = JBLabel("x")
val wrap = Align(child, HAlign.FIT, VAlign.FIT)
assertEquals(1, wrap.componentCount)
assertSame(child, wrap.getComponent(0))
}
// ------ FIT / FIT basic fill ------
fun `test FIT FIT fills assigned inner bounds`() {
val child = child(pref = 40 x 20)
val wrap = Align(child, HAlign.FIT, VAlign.FIT)
wrap.setBounds(0, 0, 200, 100)
wrap.doLayout()
assertBounds(0, 0, 200, 100, child)
}
fun `test FIT FIT respects insets`() {
val child = child(pref = 40 x 20)
val wrap = Align(child, HAlign.FIT, VAlign.FIT)
wrap.border = JBUI.Borders.empty(5, 10, 5, 10)
wrap.setBounds(0, 0, 200, 100)
wrap.doLayout()
assertBounds(10, 5, 180, 90, child)
}
// ------ FIT respects max ------
fun `test FIT FIT caps at maximum size`() {
val child = child(pref = 40 x 20, max = 60 x 30)
val wrap = Align(child, HAlign.FIT, VAlign.FIT)
wrap.setBounds(0, 0, 200, 100)
wrap.doLayout()
// available > max → capped at max, placed at top-left
assertBounds(0, 0, 60, 30, child)
}
fun `test FIT FIT expands to minimum when available between min and pref`() {
val child = child(min = 30 x 15, pref = 80 x 40, max = 200 x 100)
val wrap = Align(child, HAlign.FIT, VAlign.FIT)
wrap.setBounds(0, 0, 50, 25)
wrap.doLayout()
// available (50x25) is within [min, max], so child gets exactly available
assertBounds(0, 0, 50, 25, child)
}
fun `test FIT FIT shrinks to available when available below minimum`() {
val child = child(min = 80 x 40, pref = 80 x 40)
val wrap = Align(child, HAlign.FIT, VAlign.FIT)
wrap.setBounds(0, 0, 30, 10)
wrap.doLayout()
// cannot respect min when space is smaller
assertBounds(0, 0, 30, 10, child)
}
// ------ CENTER / CENTER ------
fun `test CENTER CENTER centers at preferred size when space sufficient`() {
val child = child(pref = 40 x 20)
val wrap = Align(child, HAlign.CENTER, VAlign.CENTER)
wrap.setBounds(0, 0, 200, 100)
wrap.doLayout()
assertBounds(80, 40, 40, 20, child)
}
fun `test CENTER CENTER coerces preferred up to minimum`() {
val child = child(min = 60 x 30, pref = 40 x 20)
val wrap = Align(child, HAlign.CENTER, VAlign.CENTER)
wrap.setBounds(0, 0, 200, 100)
wrap.doLayout()
// preferred < min → use min (60x30), centered
assertBounds(70, 35, 60, 30, child)
}
fun `test CENTER CENTER caps preferred at maximum`() {
val child = child(pref = 100 x 60, max = 40 x 20)
val wrap = Align(child, HAlign.CENTER, VAlign.CENTER)
wrap.setBounds(0, 0, 200, 100)
wrap.doLayout()
// preferred > max → use max (40x20), centered
assertBounds(80, 40, 40, 20, child)
}
fun `test CENTER CENTER fits when bounded preferred exceeds available`() {
val child = child(pref = 300 x 200)
val wrap = Align(child, HAlign.CENTER, VAlign.CENTER)
wrap.setBounds(0, 0, 100, 80)
wrap.doLayout()
assertBounds(0, 0, 100, 80, child)
}
fun `test CENTER CENTER shrinks to available when available below minimum`() {
val child = child(min = 150 x 90, pref = 150 x 90)
val wrap = Align(child, HAlign.CENTER, VAlign.CENTER)
wrap.setBounds(0, 0, 100, 60)
wrap.doLayout()
assertBounds(0, 0, 100, 60, child)
}
// ------ LEFT / TOP ------
fun `test LEFT TOP positions at top-left with bounded preferred`() {
val child = child(pref = 40 x 20)
val wrap = Align(child, HAlign.LEFT, VAlign.TOP)
wrap.setBounds(0, 0, 200, 100)
wrap.doLayout()
assertBounds(0, 0, 40, 20, child)
}
fun `test LEFT TOP respects max`() {
val child = child(pref = 100 x 60, max = 40 x 20)
val wrap = Align(child, HAlign.LEFT, VAlign.TOP)
wrap.setBounds(0, 0, 200, 100)
wrap.doLayout()
assertBounds(0, 0, 40, 20, child)
}
fun `test LEFT TOP shrinks to available`() {
val child = child(pref = 300 x 200)
val wrap = Align(child, HAlign.LEFT, VAlign.TOP)
wrap.setBounds(0, 0, 100, 80)
wrap.doLayout()
assertBounds(0, 0, 100, 80, child)
}
// ------ RIGHT / BOTTOM ------
fun `test RIGHT BOTTOM positions at bottom-right with bounded preferred`() {
val child = child(pref = 40 x 20)
val wrap = Align(child, HAlign.RIGHT, VAlign.BOTTOM)
wrap.setBounds(0, 0, 200, 100)
wrap.doLayout()
assertBounds(160, 80, 40, 20, child)
}
fun `test RIGHT BOTTOM respects max`() {
val child = child(pref = 100 x 60, max = 40 x 20)
val wrap = Align(child, HAlign.RIGHT, VAlign.BOTTOM)
wrap.setBounds(0, 0, 200, 100)
wrap.doLayout()
assertBounds(160, 80, 40, 20, child)
}
fun `test RIGHT BOTTOM shrinks to available`() {
val child = child(pref = 300 x 200)
val wrap = Align(child, HAlign.RIGHT, VAlign.BOTTOM)
wrap.setBounds(0, 0, 100, 80)
wrap.doLayout()
assertBounds(0, 0, 100, 80, child)
}
// ------ insets with edge modes ------
fun `test CENTER CENTER insets honored`() {
val child = child(pref = 40 x 20)
val wrap = Align(child, HAlign.CENTER, VAlign.CENTER)
wrap.border = JBUI.Borders.empty(10, 20, 10, 20)
wrap.setBounds(0, 0, 200, 100)
wrap.doLayout()
val ins = wrap.insets // 10,20,10,20
// inner: 160x80; child 40x20
assertBounds(ins.left + 60, ins.top + 30, 40, 20, child)
}
fun `test RIGHT BOTTOM insets honored`() {
val child = child(pref = 40 x 20)
val wrap = Align(child, HAlign.RIGHT, VAlign.BOTTOM)
wrap.border = JBUI.Borders.empty(5, 5, 5, 5)
wrap.setBounds(0, 0, 100, 80)
wrap.doLayout()
val ins = wrap.insets
// inner: 90x70; child 40x20
assertBounds(ins.left + 50, ins.top + 50, 40, 20, child)
}
// ------ wrapper preferred/min/max sizes (non-TRACK) ------
fun `test preferredSize equals bounded child pref plus insets`() {
val child = child(min = 30 x 15, pref = 80 x 40, max = 60 x 30)
val wrap = Align(child, HAlign.CENTER, VAlign.CENTER)
wrap.border = JBUI.Borders.empty(4, 6, 4, 6)
val ins = wrap.insets
// pref(80) coerced into [30,60] = 60; pref(40) coerced into [15,30] = 30
val ps = wrap.preferredSize
assertEquals(60 + ins.left + ins.right, ps.width)
assertEquals(30 + ins.top + ins.bottom, ps.height)
}
fun `test minimumSize equals child min plus insets`() {
val child = child(min = 30 x 15, pref = 80 x 40)
val wrap = Align(child, HAlign.LEFT, VAlign.TOP)
wrap.border = JBUI.Borders.empty(4, 6, 4, 6)
val ins = wrap.insets
val ms = wrap.minimumSize
assertEquals(30 + ins.left + ins.right, ms.width)
assertEquals(15 + ins.top + ins.bottom, ms.height)
}
fun `test maximumSize equals effective child max plus insets`() {
val child = child(min = 30 x 15, pref = 80 x 40, max = 60 x 30)
val wrap = Align(child, HAlign.LEFT, VAlign.TOP)
wrap.border = JBUI.Borders.empty(4, 6, 4, 6)
val ins = wrap.insets
val xs = wrap.maximumSize
assertEquals(60 + ins.left + ins.right, xs.width)
assertEquals(30 + ins.top + ins.bottom, xs.height)
}
fun `test maximumSize uses min when max is smaller than min`() {
// max < min → effective max should be at least min
val child = child(min = 50 x 30, pref = 50 x 30, max = 10 x 5)
val wrap = Align(child, HAlign.LEFT, VAlign.TOP)
val ins = wrap.insets
val xs = wrap.maximumSize
assertEquals(50 + ins.left + ins.right, xs.width)
assertEquals(30 + ins.top + ins.bottom, xs.height)
}
// ------ CenterShrinkPanel parity ------
fun `test CENTER CENTER matches old CenterShrinkPanel center-and-shrink behavior`() {
// child pref is larger than max → should center at max size, not overflow
val child = child(pref = 100 x 60, max = 40 x 20)
val wrap = Align(child, HAlign.CENTER, VAlign.CENTER)
wrap.setBounds(0, 0, 200, 100)
wrap.doLayout()
// expected: max(40x20), centered → x=(200-40)/2=80, y=(100-20)/2=40
assertBounds(80, 40, 40, 20, child)
}
// ------ TRACK / TRACK ------
fun `test TRACK TRACK fills all available regardless of child constraints`() {
val child = child(min = 10 x 5, pref = 40 x 20, max = 60 x 30)
val wrap = Align(child, HAlign.TRACK, VAlign.TRACK)
wrap.setBounds(0, 0, 200, 100)
wrap.doLayout()
assertBounds(0, 0, 200, 100, child)
}
fun `test TRACK TRACK preferred and min size are just insets`() {
val child = child(min = 50 x 30, pref = 80 x 40, max = 100 x 60)
val wrap = Align(child, HAlign.TRACK, VAlign.TRACK)
wrap.border = JBUI.Borders.empty(4, 6, 4, 6)
val ins = wrap.insets
val ps = wrap.preferredSize
val ms = wrap.minimumSize
assertEquals(ins.left + ins.right, ps.width)
assertEquals(ins.top + ins.bottom, ps.height)
assertEquals(ins.left + ins.right, ms.width)
assertEquals(ins.top + ins.bottom, ms.height)
}
fun `test TRACK TRACK max size is not capped by child max`() {
val child = child(pref = 40 x 20, max = 60 x 30)
val wrap = Align(child, HAlign.TRACK, VAlign.TRACK)
val xs = wrap.maximumSize
// wrapper max must be larger than child max since TRACK should allow any size
assertTrue("wrapper maxW ${xs.width} should exceed child maxW 60", xs.width > 60)
assertTrue("wrapper maxH ${xs.height} should exceed child maxH 30", xs.height > 30)
}
// ------ mixed TRACK + non-TRACK ------
fun `test TRACK H FIT V fills width ignores child constraints on H only`() {
val child = child(min = 30 x 15, pref = 40 x 20, max = 60 x 30)
val wrap = Align(child, HAlign.TRACK, VAlign.FIT)
wrap.setBounds(0, 0, 200, 100)
wrap.doLayout()
// H=TRACK → width=200; V=FIT → height clamped to [15,30]=30
assertBounds(0, 0, 200, 30, child)
}
fun `test TRACK H preferred is inset-only on H axis with child bounded pref on V axis`() {
val child = child(min = 30 x 15, pref = 80 x 40, max = 60 x 30)
val wrap = Align(child, HAlign.TRACK, VAlign.CENTER)
val ins = wrap.insets
val ps = wrap.preferredSize
// H=TRACK → horizontal contribution = 0
assertEquals(ins.left + ins.right, ps.width)
// V=CENTER → bounded pref height = clamp(40,[15,30]) = 30
assertEquals(30 + ins.top + ins.bottom, ps.height)
}
// ------ factory helpers ------
fun `test align extension returns Align wrapping child`() {
val child = JBLabel("x")
assertSame(child, child.align(HAlign.LEFT, VAlign.TOP).getComponent(0))
}
fun `test alignCenter produces CENTER CENTER`() {
val child = child(pref = 40 x 20)
val wrap = child.alignCenter()
wrap.setBounds(0, 0, 200, 100)
wrap.doLayout()
assertBounds(80, 40, 40, 20, child)
}
fun `test alignRight produces RIGHT with given VAlign`() {
val child = child(pref = 40 x 20)
val wrap = child.alignRight(VAlign.TOP)
wrap.setBounds(0, 0, 200, 100)
wrap.doLayout()
assertBounds(160, 0, 40, 20, child)
}
fun `test alignLeft with default FIT vertical fills height`() {
val child = child(pref = 40 x 20)
val wrap = child.alignLeft()
wrap.setBounds(0, 0, 200, 100)
wrap.doLayout()
assertBounds(0, 0, 40, 100, child)
}
fun `test alignTop with CENTER horizontal centers and pins to top`() {
val child = child(pref = 40 x 20)
val wrap = child.alignTop(HAlign.CENTER)
wrap.setBounds(0, 0, 200, 100)
wrap.doLayout()
assertBounds(80, 0, 40, 20, child)
}
fun `test alignBottom with default FIT horizontal fills width and pins to bottom`() {
val child = child(pref = 40 x 20)
val wrap = child.alignBottom()
wrap.setBounds(0, 0, 200, 100)
wrap.doLayout()
assertBounds(0, 80, 200, 20, child)
}
fun `test track fills all space and wrapper preferred is inset-only`() {
val child = child(pref = 40 x 20, max = 60 x 30)
val wrap = child.track()
wrap.setBounds(0, 0, 200, 100)
wrap.doLayout()
assertBounds(0, 0, 200, 100, child)
val ins = wrap.insets
assertEquals(ins.left + ins.right, wrap.preferredSize.width)
assertEquals(ins.top + ins.bottom, wrap.preferredSize.height)
}
fun `test trackX fills width only, V respects preferred`() {
val child = child(pref = 40 x 20)
val wrap = child.trackX(VAlign.TOP)
wrap.setBounds(0, 0, 200, 100)
wrap.doLayout()
assertBounds(0, 0, 200, 20, child)
}
fun `test trackY fills height only, H respects preferred`() {
val child = child(pref = 40 x 20)
val wrap = child.trackY(HAlign.CENTER)
wrap.setBounds(0, 0, 200, 100)
wrap.doLayout()
assertBounds(80, 0, 40, 100, child)
}
// ------ helpers ------
private infix fun Int.x(h: Int) = Dimension(this, h)
private fun child(
min: Dimension = Dimension(0, 0),
pref: Dimension,
max: Dimension = Dimension(Int.MAX_VALUE, Int.MAX_VALUE),
) = object : JBLabel("x") {
override fun getMinimumSize() = min
override fun getPreferredSize() = pref
override fun getMaximumSize() = max
}
private fun assertBounds(x: Int, y: Int, w: Int, h: Int, c: java.awt.Component) {
val b = c.bounds
assertEquals("x", x, b.x)
assertEquals("y", y, b.y)
assertEquals("width", w, b.width)
assertEquals("height", h, b.height)
}
}