diff --git a/.changeset/center-jetbrains-session-width.md b/.changeset/center-jetbrains-session-width.md new file mode 100644 index 00000000000..2ea65486c06 --- /dev/null +++ b/.changeset/center-jetbrains-session-width.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": patch +--- + +Center JetBrains session content at a 98-column readable width on wide panels. diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt index fd971f8a8c4..bcb83afd4e9 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt @@ -44,6 +44,9 @@ import ai.kilocode.client.session.ui.selection.SessionHoverCopyOverlay import ai.kilocode.client.session.ui.selection.SessionSelection import ai.kilocode.client.session.ui.style.SessionEditorStyle import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget +import ai.kilocode.client.ui.layout.HAlign +import ai.kilocode.client.ui.layout.VAlign +import ai.kilocode.client.ui.layout.align import ai.kilocode.client.session.controller.EVENT_FLUSH_MS import ai.kilocode.client.session.controller.PromptSelection import ai.kilocode.client.session.controller.SessionController @@ -452,7 +455,14 @@ class SessionUi( sessionContent.add(header, BorderLayout.NORTH) sessionContent.add(scroll.component, BorderLayout.CENTER) root.content.add(sessionContent, BorderLayout.CENTER) - root.content.add(prompt, BorderLayout.SOUTH) + root.content.add( + prompt.align( + HAlign.CENTER, + VAlign.FIT, + maxW = { SessionUiStyle.SessionLayout.readableWidth(prompt, style.transcriptFont) }, + ), + BorderLayout.SOUTH, + ) add(root, BorderLayout.CENTER) } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionLayout.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionLayout.kt index 68f3c3b0e44..fe485b92f6b 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionLayout.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionLayout.kt @@ -36,6 +36,8 @@ class SessionLayout( private val cache = IdentityHashMap() + var maxWidth: ((Container) -> Int)? = null + override fun addLayoutComponent(name: String, comp: Component) = Unit override fun removeLayoutComponent(comp: Component) { cache.remove(comp) @@ -50,7 +52,7 @@ class SessionLayout( if (!comp.isVisible) continue if (!first) h += gap(comp) first = false - val child = bounds(ins, w, comp) + val child = bounds(parent, ins, w, comp) h += measure(comp, child.width) } // w and h are already scaled px (child preferred heights + scaled gaps/insets) and @@ -70,7 +72,7 @@ class SessionLayout( if (!comp.isVisible) continue if (!first) y += gap(comp) first = false - val child = bounds(ins, w, comp) + val child = bounds(parent, ins, w, comp) val h = measure(comp, child.width) comp.setBounds(child.left, y, child.width, h) y += h @@ -106,13 +108,20 @@ class SessionLayout( return h } - private fun bounds(ins: Insets, width: Int, comp: Component): Bounds { - val view = view(comp) ?: return Bounds(ins.left, width) - if (view.sessionViewKind != SessionView.Kind.UserPrompt) return Bounds(ins.left, width) + private fun bounds(parent: Container, ins: Insets, width: Int, comp: Component): Bounds { + val lane = lane(parent, ins, width) + val view = view(comp) ?: return lane + if (view.sessionViewKind != SessionView.Kind.UserPrompt) return lane val shift = JBUI.scale(SessionUiStyle.SessionLayout.USER_PROMPT_INDENT) - val next = width - shift - if (next < JBUI.scale(SessionUiStyle.SessionLayout.USER_PROMPT_INDENT)) return Bounds(ins.left, width) - return Bounds(ins.left + shift, next) + val next = lane.width - shift + if (next < JBUI.scale(SessionUiStyle.SessionLayout.USER_PROMPT_INDENT)) return lane + return Bounds(lane.left + shift, next) + } + + private fun lane(parent: Container, ins: Insets, width: Int): Bounds { + val cap = maxWidth?.invoke(parent) ?: width + val w = minOf(width, cap.coerceAtLeast(0)) + return Bounds(ins.left + (width - w) / 2, w) } private fun insets(parent: Container): Insets { diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanel.kt index 23a2bb65196..0fa4d29099f 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanel.kt @@ -97,6 +97,9 @@ class SessionMessageListPanel( init { Disposer.register(parent, this) + (layout as? SessionLayout)?.maxWidth = { view -> + SessionUiStyle.SessionLayout.readableWidth(view, style.transcriptFont) + } applyStyle(style) model.addListener(parent) { event -> diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/empty/EmptySessionPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/empty/EmptySessionPanel.kt index 50f1bada459..dc02b961690 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/empty/EmptySessionPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/empty/EmptySessionPanel.kt @@ -55,7 +55,12 @@ class EmptySessionPanel( private val browse: (String) -> Unit = BrowserUtil::browse, private val timers: UiTimerSource = UiTimers, ) : BorderLayoutPanel(), Disposable, SessionEditorStyleTarget { - val view: Align = align(HAlign.CENTER, VAlign.CENTER) + private var style = SessionEditorStyle.current() + val view: Align = align( + HAlign.CENTER, + VAlign.CENTER, + maxW = { SessionUiStyle.SessionLayout.readableWidth(this, style.transcriptFont) }, + ) private val timer = timers.timer(ACTIVITY_MS) { syncActivity() } internal val recent = RecentsList(recents, controller) @@ -248,6 +253,7 @@ class EmptySessionPanel( } override fun applyStyle(style: SessionEditorStyle) { + this.style = style welcomeLabel.font = style.regularFont recent.applyStyle(style) revalidate() diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyle.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyle.kt index ff81a6cd395..7d86dc4866b 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyle.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyle.kt @@ -5,6 +5,7 @@ import com.intellij.ui.JBColor import com.intellij.util.ui.JBUI import com.intellij.util.ui.UIUtil import java.awt.Color +import java.awt.Component import java.awt.Font import javax.swing.UIManager @@ -75,6 +76,7 @@ object SessionUiStyle { /** Geometry for the transcript list and its scroll behavior. */ object SessionLayout { + const val READABLE_COLUMNS = 98 const val GAP = 3 const val USER_PROMPT_GAP = 10 const val TRANSCRIPT_SCROLLBAR_PADDING = 10 @@ -87,6 +89,11 @@ object SessionUiStyle { const val USER_PROMPT_INDENT = 100 const val SCROLL_INCREMENT = 48 + + fun readableWidth(component: Component, font: Font): Int { + val width = component.getFontMetrics(font).charWidth('0').coerceAtLeast(1) + return width * READABLE_COLUMNS + } } /** Shared tokens for individual transcript views and session views. */ diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/layout/Align.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/layout/Align.kt index e65a2b10408..3b59914c38c 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/layout/Align.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/layout/Align.kt @@ -21,6 +21,9 @@ enum class VAlign { TRACK, FIT, TOP, CENTER, BOTTOM } * **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. + * When a dynamic max function is supplied for an axis, these modes fill available space + * up to that max and use alignment only for the leftover space, matching CSS + * `width: 100%; max-width: ...; margin-inline: auto` behavior. * * During layout, the child is first sized on TRACK/FIT axes before preferred size * is read. This mirrors Swing layouts such as [java.awt.BorderLayout] where the @@ -42,7 +45,9 @@ class Align( child: Component, h: HAlign = HAlign.FIT, v: VAlign = VAlign.FIT, -) : JPanel(Layout(h, v)) { + maxW: (() -> Int)? = null, + maxH: (() -> Int)? = null, +) : JPanel(Layout(h, v, maxW, maxH)) { init { isOpaque = false @@ -52,6 +57,8 @@ class Align( private class Layout( private val h: HAlign, private val v: VAlign, + private val maxW: (() -> Int)?, + private val maxH: (() -> Int)?, ) : LayoutManager2 { override fun addLayoutComponent(comp: Component, constraints: Any?) = Unit @@ -66,17 +73,17 @@ class Align( val availH = maxOf(0, parent.height - ins.top - ins.bottom) val min = child.minimumSize - val max = child.maximumSize - if (probes(h) || probes(v)) { + val max = max(child) + if (probes(h) || probes(v) || maxW != null || maxH != null) { child.setSize( - if (probes(h)) probe(h, availW, min.width, max.width) else child.width, - if (probes(v)) probe(v, availH, min.height, max.height) else child.height, + if (probes(h) || maxW != null) probe(h, availW, min.width, max.width, maxW != null) else child.width, + if (probes(v) || maxH != null) probe(v, availH, min.height, max.height, maxH != null) else child.height, ) } val pref = child.preferredSize - val (w, cx) = place(h, availW, min.width, pref.width, max.width) - val (ht, cy) = place(v, availH, min.height, pref.height, max.height) + val (w, cx) = place(h, availW, min.width, pref.width, max.width, maxW != null) + val (ht, cy) = place(v, availH, min.height, pref.height, max.height, maxH != null) child.setBounds(ins.left + cx, ins.top + cy, w, ht) } @@ -95,18 +102,18 @@ class Align( val child = parent.getComponent(0) val ins = parent.insets val min = child.minimumSize - val max = child.maximumSize + val max = max(child) val availW = maxOf(0, parent.width - ins.left - ins.right) val availH = maxOf(0, parent.height - ins.top - ins.bottom) - if ((availW > 0 && probes(h)) || (availH > 0 && probes(v))) { + if ((availW > 0 && (probes(h) || maxW != null)) || (availH > 0 && (probes(v) || maxH != null))) { child.setSize( - if (availW > 0 && probes(h)) probe(h, availW, min.width, max.width) else child.width, - if (availH > 0 && probes(v)) probe(v, availH, min.height, max.height) else child.height, + if (availW > 0 && (probes(h) || maxW != null)) probe(h, availW, min.width, max.width, maxW != null) else child.width, + if (availH > 0 && (probes(v) || maxH != null)) probe(v, availH, min.height, max.height, maxH != null) else child.height, ) } val pref = child.preferredSize - val cw = if (h == HAlign.TRACK) 0 else bounded(pref.width, min.width, max.width) - val ch = if (v == VAlign.TRACK) 0 else bounded(pref.height, min.height, max.height) + val cw = if (h == HAlign.TRACK) 0 else size(h, pref.width, min.width, max.width, maxW != null) + val ch = if (v == VAlign.TRACK) 0 else size(v, pref.height, min.height, max.height, maxH != null) return Dimension(cw + ins.left + ins.right, ch + ins.top + ins.bottom) } @@ -114,19 +121,25 @@ class Align( if (target.componentCount == 0) return Dimension(Int.MAX_VALUE, Int.MAX_VALUE) val child = target.getComponent(0) val ins = target.insets + val max = max(child) val cw = if (h == HAlign.TRACK) { Int.MAX_VALUE } else { - maxOf(child.minimumSize.width, child.maximumSize.width) + ins.left + ins.right + maxOf(child.minimumSize.width, max.width) + ins.left + ins.right } val ch = if (v == VAlign.TRACK) { Int.MAX_VALUE } else { - maxOf(child.minimumSize.height, child.maximumSize.height) + ins.top + ins.bottom + maxOf(child.minimumSize.height, max.height) + ins.top + ins.bottom } return Dimension(cw, ch) } + private fun max(child: Component): Dimension { + val base = child.maximumSize + return Dimension(maxW?.invoke() ?: base.width, maxH?.invoke() ?: base.height) + } + override fun getLayoutAlignmentX(target: Container) = 0.5f override fun getLayoutAlignmentY(target: Container) = 0.5f override fun invalidateLayout(target: Container) = Unit @@ -143,7 +156,7 @@ class Align( * - FIT: size = clamp(avail, min, max), offset = 0 * - edge/center: size = clamp(boundedPref, 0, avail), offset positions according to alignment */ -private fun place(mode: Any, avail: Int, min: Int, pref: Int, max: Int): Pair { +private fun place(mode: Any, avail: Int, min: Int, pref: Int, max: Int, cap: Boolean): Pair { val effMax = maxOf(min, max) return when (mode) { HAlign.TRACK, VAlign.TRACK -> avail to 0 @@ -153,15 +166,15 @@ private fun place(mode: Any, avail: Int, min: Int, pref: Int, max: Int): Pair { - val size = minOf(bounded(pref, min, effMax), avail) + val size = minOf(size(mode, pref, min, effMax, cap), avail) size to 0 } HAlign.CENTER, VAlign.CENTER -> { - val size = minOf(bounded(pref, min, effMax), avail) + val size = minOf(size(mode, pref, min, effMax, cap), avail) size to (avail - size) / 2 } HAlign.RIGHT, VAlign.BOTTOM -> { - val size = minOf(bounded(pref, min, effMax), avail) + val size = minOf(size(mode, pref, min, effMax, cap), avail) size to (avail - size) } else -> avail to 0 @@ -170,9 +183,18 @@ private fun place(mode: Any, avail: Int, min: Int, pref: Int, max: Int): Pair Int)? = null, maxH: (() -> Int)? = null) = Align(this, h, v, maxW, maxH) diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiLayoutTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiLayoutTest.kt index b12492b70a6..e981207596e 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiLayoutTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/SessionUiLayoutTest.kt @@ -22,6 +22,7 @@ import ai.kilocode.client.session.ui.style.SessionEditorStyle import ai.kilocode.client.session.ui.header.SessionHeaderPanel import ai.kilocode.client.session.ui.style.SessionUiStyle import ai.kilocode.client.session.controller.SessionControllerEvent +import ai.kilocode.client.ui.layout.Align import ai.kilocode.rpc.dto.ChatEventDto import ai.kilocode.rpc.dto.ConfigDto import ai.kilocode.rpc.dto.KiloAppStateDto @@ -37,6 +38,7 @@ import com.intellij.ui.components.JBScrollPane import java.awt.Dimension import javax.swing.JLayeredPane import javax.swing.JPanel +import javax.swing.SwingUtilities import kotlinx.coroutines.CompletableDeferred @Suppress("UnstableApiUsage") @@ -80,7 +82,8 @@ class SessionUiLayoutTest : SessionUiTestBase() { val connection = find(ui) val prompt = find(ui) - assertSame(root.content, prompt.parent) + assertTrue(prompt.parent is Align) + assertSame(root.content, prompt.parent.parent) assertSame(root.overlay, connection.parent) assertTrue(root.overlay.components.any { it is SessionAccountOverlay }) assertFalse(root.content.components.contains(connection)) @@ -245,15 +248,17 @@ class SessionUiLayoutTest : SessionUiTestBase() { showConnection() layout() + val point = promptPoint(root, prompt) assertTrue(connection.isVisible) assertSame(root.overlay, connection.parent) - assertEquals(prompt.x, connection.x) + assertEquals(point.x, connection.x) assertEquals(prompt.width, connection.width) - assertEquals(prompt.y - SessionUiStyle.View.Outline.width(), connection.y + connection.height) + assertEquals(point.y - SessionUiStyle.View.Outline.width(), connection.y + connection.height) } fun `test expanded connection panel remains anchored above prompt`() { + val root = find(ui) val connection = find(ui) val prompt = find(ui) @@ -266,7 +271,7 @@ class SessionUiLayoutTest : SessionUiTestBase() { layout() assertTrue(connection.detailsVisible()) - assertEquals(prompt.y - SessionUiStyle.View.Outline.width(), connection.y + connection.height) + assertEquals(promptPoint(root, prompt).y - SessionUiStyle.View.Outline.width(), connection.y + connection.height) } fun `test connection panel is unaffected by active question view`() { @@ -276,6 +281,7 @@ class SessionUiLayoutTest : SessionUiTestBase() { layout() val connection = find(ui) val prompt = find(ui) + val root = find(ui) val top = connection.y controller().model.setState(questionStateChanged()) @@ -284,7 +290,7 @@ class SessionUiLayoutTest : SessionUiTestBase() { assertTrue(find(ui).isVisible) assertSame(find(ui), find(ui).parent) assertEquals(top, connection.y) - assertEquals(prompt.y - SessionUiStyle.View.Outline.width(), connection.y + connection.height) + assertEquals(promptPoint(root, prompt).y - SessionUiStyle.View.Outline.width(), connection.y + connection.height) assertSame(find(ui), scrollView()) } @@ -295,6 +301,7 @@ class SessionUiLayoutTest : SessionUiTestBase() { layout() val connection = find(ui) val prompt = find(ui) + val root = find(ui) val top = connection.y controller().model.setState(permissionStateChanged()) @@ -303,7 +310,7 @@ class SessionUiLayoutTest : SessionUiTestBase() { assertTrue(find(ui).isVisible) assertSame(find(ui), find(ui).parent) assertEquals(top, connection.y) - assertEquals(prompt.y - SessionUiStyle.View.Outline.width(), connection.y + connection.height) + assertEquals(promptPoint(root, prompt).y - SessionUiStyle.View.Outline.width(), connection.y + connection.height) assertSame(find(ui), scrollView()) } @@ -764,6 +771,9 @@ class SessionUiLayoutTest : SessionUiTestBase() { .single() .let { it as javax.swing.JComponent } + private fun promptPoint(root: SessionRootPanel, prompt: PromptPanel) = + SwingUtilities.convertPoint(prompt.parent, prompt.x, prompt.y, root.overlay) + private class Row(override val sessionViewKind: SessionView.Kind) : JPanel(), SessionView { override fun getPreferredSize() = Dimension(100, 10) } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionLayoutTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionLayoutTest.kt index ced62e863ba..9d2170710cf 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionLayoutTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionLayoutTest.kt @@ -185,6 +185,56 @@ class SessionLayoutTest : BasePlatformTestCase() { assertEquals(20, child.height) } + fun `test max width centers default session view in wide container`() { + val p = panel(width = 500) + (p.layout as SessionLayout).maxWidth = { 300 } + val child = view(height = 20, kind = SessionView.Kind.Default) + p.add(child) + p.doLayout() + + assertEquals(100, child.x) + assertEquals(300, child.width) + assertEquals(20, child.height) + } + + fun `test max width fills narrow container`() { + val p = panel(width = 250) + (p.layout as SessionLayout).maxWidth = { 300 } + val child = view(height = 20, kind = SessionView.Kind.Default) + p.add(child) + p.doLayout() + + assertEquals(0, child.x) + assertEquals(250, child.width) + assertEquals(20, child.height) + } + + fun `test max width centers user prompt indent inside lane`() { + val p = panel(width = 500) + (p.layout as SessionLayout).maxWidth = { 300 } + val child = view(height = 20, kind = SessionView.Kind.UserPrompt) + p.add(child) + p.doLayout() + + assertEquals(100 + JBUI.scale(SessionUiStyle.SessionLayout.USER_PROMPT_INDENT), child.x) + assertEquals(300 - JBUI.scale(SessionUiStyle.SessionLayout.USER_PROMPT_INDENT), child.width) + assertEquals(20, child.height) + } + + fun `test max width measures child height at capped width`() { + val p = panel(width = 500) + (p.layout as SessionLayout).maxWidth = { 300 } + val child = object : JPanel() { + override fun getPreferredSize(): Dimension = Dimension(0, if (width == 300) 20 else 80) + } + p.add(child) + p.doLayout() + + assertEquals(100, child.x) + assertEquals(300, child.width) + assertEquals(20, child.height) + } + // ---- invisible children ------ fun `test invisible child is skipped in layout`() { diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/layout/AlignTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/layout/AlignTest.kt index 22397d6b66c..0e8acb13088 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/layout/AlignTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/layout/AlignTest.kt @@ -298,6 +298,63 @@ class AlignTest : BasePlatformTestCase() { assertEquals(30 + ins.top + ins.bottom, ps.height) } + // ------ function max ------ + + fun `test CENTER with max function fills cap and centers`() { + val child = child(pref = 40 x 20) + val wrap = Align(child, HAlign.CENTER, VAlign.TOP, maxW = { 120 }) + wrap.setBounds(0, 0, 300, 100) + wrap.doLayout() + + assertBounds(90, 0, 120, 20, child) + } + + fun `test CENTER with max function fills available below cap`() { + val child = child(pref = 40 x 20) + val wrap = Align(child, HAlign.CENTER, VAlign.TOP, maxW = { 120 }) + wrap.setBounds(0, 0, 80, 100) + wrap.doLayout() + + assertBounds(0, 0, 80, 20, child) + } + + fun `test RIGHT with max function pins capped width to right`() { + val child = child(pref = 40 x 20) + val wrap = Align(child, HAlign.RIGHT, VAlign.TOP, maxW = { 120 }) + wrap.setBounds(0, 0, 300, 100) + wrap.doLayout() + + assertBounds(180, 0, 120, 20, child) + } + + fun `test max function is evaluated on each layout`() { + var cap = 120 + val child = child(pref = 40 x 20) + val wrap = Align(child, HAlign.CENTER, VAlign.TOP, maxW = { cap }) + wrap.setBounds(0, 0, 300, 100) + wrap.doLayout() + assertBounds(90, 0, 120, 20, child) + + cap = 60 + wrap.doLayout() + + assertBounds(120, 0, 60, 20, child) + } + + fun `test max function probes capped width before measuring height`() { + val child = object : JBLabel("x") { + override fun getMinimumSize() = Dimension(0, 0) + override fun getPreferredSize() = Dimension(20, if (width == 120) 12 else 60) + override fun getMaximumSize() = Dimension(Int.MAX_VALUE, Int.MAX_VALUE) + } + val wrap = Align(child, HAlign.CENTER, VAlign.TOP, maxW = { 120 }) + + wrap.setBounds(0, 0, 300, 100) + wrap.doLayout() + + assertBounds(90, 0, 120, 12, child) + } + // ------ align() factory ------ fun `test align extension returns Align wrapping child`() {