Repository: ajalt/mordant
Pull request #45 (2021-12-05 18:22:43)

Add row and column builders


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 007eeef7..0748cccc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,8 @@
 ## Unreleased
 ### Added
 - `Spinner` widget that displays a looping animation
+- `EmptyEidget` widget that can be used as a placeholder in layouts
+- `row{}` and `column{}` widget layouts that create a single row/column of widgets
 
 ### Fixed
 - Reduced flickering on high frame rate animations
diff --git a/README.md b/README.md
index 29c78e13..f487d05f 100644
--- a/README.md
+++ b/README.md
@@ -151,6 +151,11 @@ table {
 }
 ```
 
+## Layout
+
+If you need to lay out multiple widgets or strings, you can use the `grid` builder, which has an API similar to `table`,
+but doesn't apply styling by default. There are also the `row` and `column` builders if you don't need a full grid.
+
 ![](docs/img/complex_table.png)
 
 ## Markdown
@@ -243,6 +248,26 @@ dependencies {
 
 ###### If you're using Maven instead of Gradle, use `<artifactId>mordant-jvm</artifactId>`
 
+#### Snapshots
+
+<details>
+<summary>Snapshot builds are also available</summary>
+
+<a href="https://oss.sonatype.org/content/repositories/snapshots/com/github/ajalt/mordant/mordant"><img src="https://img.shields.io/nexus/s/com.github.ajalt.mordant/mordant?color=blue&label=latest%20shapshot&server=https%3A%2F%2Foss.sonatype.org"/></a>
+
+<p>
+You'll need to add the Sonatype snapshots repository:
+
+```kotlin
+repositories {
+    maven {
+        url = uri("https://oss.sonatype.org/content/repositories/snapshots/")
+    }
+}
+```
+</p>
+</details>
+
 ## License
 
     Copyright 2018-2021 AJ Alt
diff --git a/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/rendering/Widget.kt b/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/rendering/Widget.kt
index c39d4e53..c55e391a 100644
--- a/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/rendering/Widget.kt
+++ b/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/rendering/Widget.kt
@@ -1,6 +1,5 @@
 package com.github.ajalt.mordant.rendering
 
-import com.github.ajalt.mordant.internal.EMPTY_LINES
 import com.github.ajalt.mordant.terminal.Terminal
 
 interface Widget {
@@ -8,7 +7,3 @@ interface Widget {
     fun render(t: Terminal, width: Int = t.info.width): Lines
 }
 
-internal object EmptyWidget : Widget {
-    override fun measure(t: Terminal, width: Int) = WidthRange(0, 0)
-    override fun render(t: Terminal, width: Int) = EMPTY_LINES
-}
diff --git a/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/table/TableDsl.kt b/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/table/TableDsl.kt
index 84377278..46049561 100644
--- a/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/table/TableDsl.kt
+++ b/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/table/TableDsl.kt
@@ -49,7 +49,12 @@ sealed class ColumnWidth {
     /** The column will fit to the size of its content */
     object Auto : ColumnWidth()
 
-    /** The column will have a fixed [width] */
+    /**
+     * The column will have a fixed [width].
+     *
+     * The width includes padding, so increasing horizontal padding of a fixed column will decrease content width rather
+     * than expand the column.
+     */
     class Fixed(val width: Int) : ColumnWidth() {
         init {
             require(width > 0) { "width must be greater than zero" }
@@ -67,7 +72,7 @@ sealed class ColumnWidth {
             require(weight > 0) { "weight must be greater than zero" }
         }
 
-        constructor(weight: Int) : this(weight.toFloat())
+        constructor(weight: Number) : this(weight.toFloat())
     }
 }
 
@@ -111,12 +116,8 @@ interface TableBuilder : CellStyleBuilder {
     fun footer(init: SectionBuilder.() -> Unit)
 }
 
-
 @MordantDsl
-interface SectionBuilder : CellStyleBuilder {
-    /** Configure a single column, which the first column at index 0. */
-    fun column(i: Int, init: CellStyleBuilder.() -> Unit)
-
+interface RowHolderBuilder {
     /** Add styles to alternating rows. If there are more rows than styles, the styles will loop. */
     fun rowStyles(style1: TextStyle, style2: TextStyle, vararg styles: TextStyle)
 
@@ -132,21 +133,15 @@ interface SectionBuilder : CellStyleBuilder {
 }
 
 @MordantDsl
-interface GridBuilder : CellStyleBuilder {
+interface SectionBuilder : CellStyleBuilder, RowHolderBuilder {
+    /** Configure a single column, which the first column at index 0. */
+    fun column(i: Int, init: CellStyleBuilder.() -> Unit)
+}
+
+@MordantDsl
+interface GridBuilder : CellStyleBuilder, RowHolderBuilder {
     /** Configure a single column, with the first column at index 0. */
     fun column(i: Int, init: ColumnBuilder.() -> Unit)
-
-    /** Add styles to alternating rows. If there are more rows than styles, the styles will loop. */
-    fun rowStyles(style1: TextStyle, style2: TextStyle, vararg styles: TextStyle)
-
-    /** Add all [cells] from an iterable. */
-    fun rowFrom(cells: Iterable<Any?>, init: RowBuilder.() -> Unit = {})
-
-    /** Add a row with one or more cells. */
-    fun row(vararg cells: Any?, init: RowBuilder.() -> Unit = {})
-
-    /** Add a row. */
-    fun row(init: RowBuilder.() -> Unit)
 }
 
 @MordantDsl
@@ -165,6 +160,31 @@ interface RowBuilder : CellStyleBuilder {
     fun cell(content: Any?, init: CellBuilder.() -> Unit = {})
 }
 
+@MordantDsl
+interface SingleLineBuilder : CellStyleBuilder {
+    /** Add multiple cells to this row */
+    fun cells(cell1: Any?, cell2: Any?, vararg cells: Any?, init: CellStyleBuilder.() -> Unit = {})
+
+    /** Add all [cells] from an iterable to this row */
+    fun cellsFrom(cells: Iterable<Any?>, init: CellStyleBuilder.() -> Unit = {})
+
+    /** Add a cell to this row.
+     *
+     * The [content] can be a [Widget] to render, or any other type of object which will be
+     * converted to a string.
+     */
+    fun cell(content: Any?, init: CellStyleBuilder.() -> Unit = {})
+}
+
+@MordantDsl
+interface SingleRowBuilder : SingleLineBuilder {
+    /** Configure a single column, with the first column at index 0. */
+    fun column(i: Int, init: ColumnBuilder.() -> Unit)
+}
+
+@MordantDsl
+interface SingleColumnBuilder : ColumnBuilder, SingleLineBuilder
+
 @MordantDsl
 interface CellBuilder : CellStyleBuilder {
     /**
@@ -234,3 +254,42 @@ fun grid(init: GridBuilder.() -> Unit): Widget {
 
     return TableLayout(tableBuilder).buildTable()
 }
+
+/**
+ * Build a row widget that can be used to lay out text and other widgets.
+ *
+ * This builder functions like a single row in a [grid]. Cells have optional [padding], which sets the default left
+ * padding of all cells after the first.
+ */
+fun row(padding: Int = 1, init: SingleRowBuilder.() -> Unit): Widget {
+    val tableBuilder = TableBuilderInstance().apply {
+        borders = Borders.NONE
+        this.padding = Padding.of(left = padding)
+        val b = SingleRowBuilderInstance(bodySection)
+        b.init()
+        bodySection.rows += b.row
+        columns.putAll(b.columns)
+        column(0) { this.padding = this.padding ?: Padding.none() }
+    }
+
+    return TableLayout(tableBuilder).buildTable()
+}
+
+
+/**
+ * Build a column widget that can be used to lay out text and other widgets.
+ *
+ * This builder functions like a single column in a [grid]. Cells have optional [padding], which sets the default top
+ * padding of all cells after the first.
+ */
+fun column(padding: Int = 0, init: SingleColumnBuilder.() -> Unit): Widget {
+    val tableBuilder = TableBuilderInstance().apply {
+        borders = Borders.NONE
+        this.padding = Padding.none()
+        val b = SingleColumnBuilderInstance(bodySection, padding)
+        b.init()
+        column(0) { width = b.width }
+    }
+
+    return TableLayout(tableBuilder).buildTable()
+}
diff --git a/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/table/TableDslInstances.kt b/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/table/TableDslInstances.kt
index 7811cc4b..a946ffcc 100644
--- a/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/table/TableDslInstances.kt
+++ b/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/table/TableDslInstances.kt
@@ -122,6 +122,62 @@ internal class GridBuilderInstance(
     }
 }
 
+@MordantDsl
+internal class SingleRowBuilderInstance(
+    private val section: SectionBuilder,
+) : SingleRowBuilder, CellStyleBuilder by section {
+    val columns = mutableMapOf<Int, ColumnBuilder>()
+    val row = RowBuilderInstance(mutableListOf())
+
+    override fun cells(cell1: Any?, cell2: Any?, vararg cells: Any?, init: CellStyleBuilder.() -> Unit) {
+        row.cells(cell1, cell2, *cells, init = init)
+    }
+
+    override fun cellsFrom(cells: Iterable<Any?>, init: CellStyleBuilder.() -> Unit) {
+        row.cellsFrom(cells, init)
+    }
+
+    override fun cell(content: Any?, init: CellStyleBuilder.() -> Unit) {
+        row.cell(content, init)
+    }
+
+    override fun column(i: Int, init: ColumnBuilder.() -> Unit) = initColumn(columns, i, ColumnBuilderInstance(), init)
+}
+
+@MordantDsl
+internal class SingleColumnBuilderInstance(
+    private val section: SectionBuilder,
+    private val defaultPadding: Int,
+) : SingleColumnBuilder, CellStyleBuilder by section {
+    override var width: ColumnWidth = ColumnWidth.Auto
+    private var empty = true
+
+    override fun cells(cell1: Any?, cell2: Any?, vararg cells: Any?, init: CellStyleBuilder.() -> Unit) {
+        section.row(cell1, init = pad(init))
+        section.row(cell2, init = pad(init))
+        cells.forEach { section.row(it, init = pad(init)) }
+    }
+
+    override fun cellsFrom(cells: Iterable<Any?>, init: CellStyleBuilder.() -> Unit) {
+        cells.forEach { section.row(it, init = pad(init)) }
+    }
+
+    override fun cell(content: Any?, init: CellStyleBuilder.() -> Unit) {
+        section.row(content, init = pad(init))
+    }
+
+    private fun pad(init: CellStyleBuilder.() -> Unit): CellStyleBuilder.() -> Unit = {
+        if (defaultPadding > 0) {
+            if (empty) {
+                empty = false
+            } else {
+                padding = Padding.of(top = defaultPadding)
+            }
+        }
+        init()
+    }
+}
+
 @MordantDsl
 internal class RowBuilderInstance(
     val cells: MutableList<CellBuilderInstance>,
diff --git a/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/widgets/Caption.kt b/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/widgets/Caption.kt
index a2b7b919..adf461e4 100644
--- a/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/widgets/Caption.kt
+++ b/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/widgets/Caption.kt
@@ -3,7 +3,7 @@ package com.github.ajalt.mordant.widgets
 import com.github.ajalt.mordant.rendering.*
 import com.github.ajalt.mordant.terminal.Terminal
 
-/** Add a top and/or bottom caption to [content] */
+/** Add a [top] and/or [bottom] caption to [content] */
 class Caption(
     val content: Widget,
     val top: Widget? = null,
diff --git a/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/widgets/EmptyWidget.kt b/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/widgets/EmptyWidget.kt
new file mode 100644
index 00000000..31972baa
--- /dev/null
+++ b/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/widgets/EmptyWidget.kt
@@ -0,0 +1,16 @@
+package com.github.ajalt.mordant.widgets
+
+import com.github.ajalt.mordant.internal.EMPTY_LINES
+import com.github.ajalt.mordant.rendering.Widget
+import com.github.ajalt.mordant.rendering.WidthRange
+import com.github.ajalt.mordant.terminal.Terminal
+
+/**
+ * A widget with 0 width, 0 height, and no content.
+ *
+ * Can be used as a placeholder in layouts etc.
+ */
+object EmptyWidget : Widget {
+    override fun measure(t: Terminal, width: Int) = WidthRange(0, 0)
+    override fun render(t: Terminal, width: Int) = EMPTY_LINES
+}
diff --git a/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/widgets/HorizontalRule.kt b/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/widgets/HorizontalRule.kt
index 9cfbeca5..6bbd6f6c 100644
--- a/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/widgets/HorizontalRule.kt
+++ b/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/widgets/HorizontalRule.kt
@@ -4,6 +4,15 @@ import com.github.ajalt.mordant.internal.*
 import com.github.ajalt.mordant.rendering.*
 import com.github.ajalt.mordant.terminal.Terminal
 
+/**
+ * A line drawn across the terminal, with an optional [title].
+ *
+ * @property title An optional widget to draw inside the rule
+ * @property ruleCharacter The string to draw the rule with, may be one or more characters. By default, uses the theme value `hr.rule`.
+ * @property ruleStyle The style of the rule. By default, uses the theme value `hr.rule`.
+ * @property titleAlign If [title] is defined, how to align it within the rule
+ * @property titlePadding The amount of space to draw between the [title] and the rule. By default, uses the theme value `hr.title.padding`.
+ */
 class HorizontalRule internal constructor(
     private val title: Widget,
     private val ruleCharacter: ThemeString,
diff --git a/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/widgets/OrderedList.kt b/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/widgets/OrderedList.kt
index 623e47d0..24669ef8 100644
--- a/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/widgets/OrderedList.kt
+++ b/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/widgets/OrderedList.kt
@@ -5,6 +5,14 @@ import com.github.ajalt.mordant.rendering.*
 import com.github.ajalt.mordant.terminal.Terminal
 import kotlin.math.log10
 
+/**
+ * A numbered list of widgets.
+ *
+ * @property listEntries The content of the list
+ * @property numberStyle The style of the list numbers. Defaults to the theme value `list.number`.
+ * @property numberSeparator The string to print between the list numbers and the widgets. Defaults to the theme value
+ *   `list.number.separator`
+ */
 class OrderedList private constructor(
     private val listEntries: List<Widget>,
     private val numberStyle: ThemeStyle,
diff --git a/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/widgets/ProgressBar.kt b/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/widgets/ProgressBar.kt
index 85e46e46..0f55f9fe 100644
--- a/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/widgets/ProgressBar.kt
+++ b/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/widgets/ProgressBar.kt
@@ -66,7 +66,7 @@ class ProgressBar private constructor(
 
     override fun measure(t: Terminal, width: Int): WidthRange {
         val w = width(width)
-        return WidthRange(w.coerceAtMost(4), w)
+        return WidthRange(w.coerceAtMost(3), w)
     }
 
     override fun render(t: Terminal, width: Int): Lines {
diff --git a/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/widgets/ProgressCell.kt b/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/widgets/ProgressCell.kt
index 4d040361..c21d1a07 100644
--- a/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/widgets/ProgressCell.kt
+++ b/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/widgets/ProgressCell.kt
@@ -61,7 +61,7 @@ internal class SpinnerProgressCell(
 
 internal class PercentageProgressCell : ProgressCell {
     override val animationRate: AnimationRate get() = AnimationRate.TEXT
-    override val columnWidth: ColumnWidth get() = ColumnWidth.Fixed(4)
+    override val columnWidth: ColumnWidth get() = ColumnWidth.Fixed(4) // " 100%"
     override fun ProgressState.makeWidget(): Widget {
         val percent = when {
             total == null || total <= 0 -> 0
@@ -78,6 +78,8 @@ internal class CompletedProgressCell(
 ) : ProgressCell {
     override val animationRate: AnimationRate get() = AnimationRate.TEXT
     override val columnWidth: ColumnWidth
+        // " 100.0M"
+        // " 100.0/200.0M"
         get() = ColumnWidth.Fixed((if (includeTotal) 12 else 6) + suffix.length)
 
     override fun ProgressState.makeWidget(): Widget {
@@ -98,7 +100,7 @@ internal class SpeedProgressCell(
     private val style: TextStyle,
 ) : ProgressCell {
     override val animationRate: AnimationRate get() = AnimationRate.TEXT
-    override val columnWidth: ColumnWidth get() = ColumnWidth.Fixed(6 + suffix.length)
+    override val columnWidth: ColumnWidth get() = ColumnWidth.Fixed(6 + suffix.length) // " 100.0M"
 
     override fun ProgressState.makeWidget(): Widget {
         val t = when {
@@ -114,7 +116,7 @@ internal class EtaProgressCell(
     private val style: TextStyle,
 ) : ProgressCell {
     override val animationRate: AnimationRate get() = AnimationRate.TEXT
-    override val columnWidth: ColumnWidth get() = ColumnWidth.Fixed(7 + prefix.length)
+    override val columnWidth: ColumnWidth get() = ColumnWidth.Fixed(7 + prefix.length) // " 0:00:02"
 
     override fun ProgressState.makeWidget(): Widget {
         val eta = if (total == null) 0.0 else (total - completed) / completedPerSecond
diff --git a/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/widgets/ProgressLayout.kt b/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/widgets/ProgressLayout.kt
index fa25f5a2..4786b97d 100644
--- a/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/widgets/ProgressLayout.kt
+++ b/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/widgets/ProgressLayout.kt
@@ -4,9 +4,8 @@ import com.github.ajalt.mordant.internal.DEFAULT_STYLE
 import com.github.ajalt.mordant.rendering.TextAlign
 import com.github.ajalt.mordant.rendering.TextStyle
 import com.github.ajalt.mordant.rendering.Widget
-import com.github.ajalt.mordant.table.Borders
 import com.github.ajalt.mordant.table.ColumnWidth
-import com.github.ajalt.mordant.table.grid
+import com.github.ajalt.mordant.table.row
 
 open class ProgressBuilder internal constructor() {
     var padding: Int = 2
@@ -92,22 +91,15 @@ class ProgressLayout internal constructor(
             completedPerSecond = cps,
             elapsedSeconds = elapsedSeconds,
         )
-        return grid {
-            rowFrom(cells.map { it.run { state.run { makeWidget() } } })
+        return row(paddingSize) {
             align = TextAlign.RIGHT
-            borders = Borders.NONE
+            cellsFrom(cells.map { it.run { state.run { makeWidget() } } })
             cells.forEachIndexed { i, it ->
                 column(i) {
-                    padding = when (i) {
-                        cells.lastIndex -> Padding.none()
-                        else -> Padding.of(right = paddingSize)
-                    }
-                    // Expand fixed columns to account for padding
-                    width = when (i) {
-                        cells.lastIndex -> it.columnWidth
-                        else -> (it.columnWidth as? ColumnWidth.Fixed)
-                            ?.let { ColumnWidth.Fixed(it.width + paddingSize) }
-                            ?: it.columnWidth
+                    width = when (val w = it.columnWidth) {
+                        // The fixed width cells don't include padding, so add it here
+                        is ColumnWidth.Fixed -> ColumnWidth.Fixed(w.width + paddingSize)
+                        else -> w
                     }
                 }
             }
diff --git a/mordant/src/jvmMain/kotlin/com/github/ajalt/mordant/animation/Animation.kt b/mordant/src/jvmMain/kotlin/com/github/ajalt/mordant/animation/Animation.kt
index 8a287546..ced60515 100644
--- a/mordant/src/jvmMain/kotlin/com/github/ajalt/mordant/animation/Animation.kt
+++ b/mordant/src/jvmMain/kotlin/com/github/ajalt/mordant/animation/Animation.kt
@@ -5,6 +5,7 @@ import com.github.ajalt.mordant.terminal.ExperimentalTerminalApi
 import com.github.ajalt.mordant.terminal.PrintRequest
 import com.github.ajalt.mordant.terminal.Terminal
 import com.github.ajalt.mordant.terminal.TerminalInterceptor
+import com.github.ajalt.mordant.widgets.EmptyWidget
 import com.github.ajalt.mordant.widgets.RawWidget
 import com.github.ajalt.mordant.widgets.Text
 


Test patch:
diff --git a/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/rendering/TextAlignmentTest.kt b/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/rendering/TextAlignmentTest.kt
index 7d4ef293..56011119 100644
--- a/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/rendering/TextAlignmentTest.kt
+++ b/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/rendering/TextAlignmentTest.kt
@@ -90,6 +90,6 @@ class TextAlignmentTest : RenderingTest() {
             (blue on white)(text),
             whitespace = Whitespace.PRE_WRAP,
             align = align,
-        ), ex, trimIndent = false, width = width)
+        ), ex, width = width)
     }
 }
diff --git a/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/rendering/TextOverflowWrapTest.kt b/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/rendering/TextOverflowWrapTest.kt
index 1d2986d1..6176caf9 100644
--- a/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/rendering/TextOverflowWrapTest.kt
+++ b/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/rendering/TextOverflowWrapTest.kt
@@ -46,6 +46,6 @@ class TextOverflowWrapTest : RenderingTest() {
             whitespace = Whitespace.NORMAL,
             align = TextAlign.NONE,
             overflowWrap = wrap
-        ), expected.trimMargin(), trimIndent = false, width = 23)
+        ), expected, width = 23)
     }
 }
diff --git a/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/rendering/TextWhitespaceTest.kt b/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/rendering/TextWhitespaceTest.kt
index fd8385ed..601638e1 100644
--- a/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/rendering/TextWhitespaceTest.kt
+++ b/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/rendering/TextWhitespaceTest.kt
@@ -62,7 +62,7 @@ class TextWhitespaceTest : RenderingTest() {
             """
                 |a
                 | b
-                """.trimMargin(),
+                """,
             width = 2
         )
     }
@@ -80,6 +80,6 @@ class TextWhitespaceTest : RenderingTest() {
             whitespace = ws,
             align = NONE,
             tabWidth = 4
-        ), expected.trimMargin(), trimIndent = false, width = width)
+        ), expected, width = width)
     }
 }
diff --git a/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/rendering/table/TableAlignmentTest.kt b/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/table/TableAlignmentTest.kt
similarity index 99%
rename from mordant/src/commonTest/kotlin/com/github/ajalt/mordant/rendering/table/TableAlignmentTest.kt
rename to mordant/src/commonTest/kotlin/com/github/ajalt/mordant/table/TableAlignmentTest.kt
index 41a53ea5..b8099e60 100644
--- a/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/rendering/table/TableAlignmentTest.kt
+++ b/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/table/TableAlignmentTest.kt
@@ -1,4 +1,4 @@
-package com.github.ajalt.mordant.rendering.table
+package com.github.ajalt.mordant.table
 
 import com.github.ajalt.mordant.rendering.*
 import com.github.ajalt.mordant.rendering.TextAlign.*
@@ -444,7 +444,7 @@ class TableAlignmentTest : RenderingTest() {
                 }
                 rowFrom(listOf("·······", "·"))
             }
-        }, expected)
+        }, expected.trimIndent())
     }
 
     private fun doWidgetTest(verticalAlign: VerticalAlign, align: TextAlign, expected: String) {
@@ -466,6 +466,6 @@ class TableAlignmentTest : RenderingTest() {
                 }
                 row("····", "·")
             }
-        }, expected)
+        }, expected.trimIndent())
     }
 }
diff --git a/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/rendering/table/TableBorderStyleTest.kt b/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/table/TableBorderStyleTest.kt
similarity index 99%
rename from mordant/src/commonTest/kotlin/com/github/ajalt/mordant/rendering/table/TableBorderStyleTest.kt
rename to mordant/src/commonTest/kotlin/com/github/ajalt/mordant/table/TableBorderStyleTest.kt
index ccfc2b51..6cdebedf 100644
--- a/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/rendering/table/TableBorderStyleTest.kt
+++ b/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/table/TableBorderStyleTest.kt
@@ -1,4 +1,4 @@
-package com.github.ajalt.mordant.rendering.table
+package com.github.ajalt.mordant.table
 
 import com.github.ajalt.mordant.rendering.BorderStyle
 import com.github.ajalt.mordant.table.Borders.*
@@ -288,6 +288,6 @@ class TableBorderStyleTest : RenderingTest() {
                 transitionRow(false)
                 sectionRows()
             }
-        }, expected, trimIndent)
+        }, if (trimIndent) expected.trimIndent() else expected, trimMargin = false)
     }
 }
diff --git a/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/rendering/table/TableBorderTest.kt b/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/table/TableBorderTest.kt
similarity index 81%
rename from mordant/src/commonTest/kotlin/com/github/ajalt/mordant/rendering/table/TableBorderTest.kt
rename to mordant/src/commonTest/kotlin/com/github/ajalt/mordant/table/TableBorderTest.kt
index 3c0adaea..5e7a0812 100644
--- a/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/rendering/table/TableBorderTest.kt
+++ b/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/table/TableBorderTest.kt
@@ -1,4 +1,4 @@
-package com.github.ajalt.mordant.rendering.table
+package com.github.ajalt.mordant.table
 
 import com.github.ajalt.mordant.table.Borders
 import com.github.ajalt.mordant.table.Borders.*
@@ -12,7 +12,7 @@ class TableBorderTest : RenderingTest() {
     fun NONE() = doTest(NONE,
         """
         | × 
-        """.trimMargin()
+        """
     )
 
     @Test
@@ -20,14 +20,14 @@ class TableBorderTest : RenderingTest() {
         """
         | × 
         |───
-        """.trimMargin()
+        """
     )
 
     @Test
     fun RIGHT() = doTest(RIGHT,
         """
         | × │
-        """.trimMargin()
+        """
     )
 
     @Test
@@ -35,7 +35,7 @@ class TableBorderTest : RenderingTest() {
         """
         | × │
         |───┘
-        """.trimMargin()
+        """
     )
 
     @Test
@@ -43,7 +43,7 @@ class TableBorderTest : RenderingTest() {
         """
         |───
         | × 
-        """.trimMargin()
+        """
     )
 
     @Test
@@ -52,7 +52,7 @@ class TableBorderTest : RenderingTest() {
         |───
         | × 
         |───
-        """.trimMargin()
+        """
     )
 
     @Test
@@ -60,7 +60,7 @@ class TableBorderTest : RenderingTest() {
         """
         |───┐
         | × │
-        """.trimMargin()
+        """
     )
 
     @Test
@@ -69,14 +69,14 @@ class TableBorderTest : RenderingTest() {
         |───┐
         | × │
         |───┘
-        """.trimMargin()
+        """
     )
 
     @Test
     fun LEFT() = doTest(LEFT,
         """
         |│ × 
-        """.trimMargin()
+        """
     )
 
     @Test
@@ -84,14 +84,14 @@ class TableBorderTest : RenderingTest() {
         """
         |│ × 
         |└───
-        """.trimMargin()
+        """
     )
 
     @Test
     fun LEFT_RIGHT() = doTest(LEFT_RIGHT,
         """
         |│ × │
-        """.trimMargin()
+        """
     )
 
     @Test
@@ -99,7 +99,7 @@ class TableBorderTest : RenderingTest() {
         """
         |│ × │
         |└───┘
-        """.trimMargin()
+        """
     )
 
     @Test
@@ -107,7 +107,7 @@ class TableBorderTest : RenderingTest() {
         """
         |┌───
         |│ × 
-        """.trimMargin()
+        """
     )
 
     @Test
@@ -116,7 +116,7 @@ class TableBorderTest : RenderingTest() {
         |┌───
         |│ × 
         |└───
-        """.trimMargin()
+        """
     )
 
     @Test
@@ -124,7 +124,7 @@ class TableBorderTest : RenderingTest() {
         """
         |┌───┐
         |│ × │
-        """.trimMargin()
+        """
     )
 
     @Test
@@ -133,7 +133,7 @@ class TableBorderTest : RenderingTest() {
         |┌───┐
         |│ × │
         |└───┘
-        """.trimMargin()
+        """
     )
 
     private fun doTest(borders: Borders, expected: String) = checkRender(table {
@@ -141,5 +141,5 @@ class TableBorderTest : RenderingTest() {
         body {
             row("×")
         }
-    }, expected, trimIndent = false)
+    }, expected)
 }
diff --git a/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/rendering/table/TableColumnWidthTest.kt b/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/table/TableColumnWidthTest.kt
similarity index 96%
rename from mordant/src/commonTest/kotlin/com/github/ajalt/mordant/rendering/table/TableColumnWidthTest.kt
rename to mordant/src/commonTest/kotlin/com/github/ajalt/mordant/table/TableColumnWidthTest.kt
index bd9d29e4..2decd0b1 100644
--- a/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/rendering/table/TableColumnWidthTest.kt
+++ b/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/table/TableColumnWidthTest.kt
@@ -1,4 +1,4 @@
-package com.github.ajalt.mordant.rendering.table
+package com.github.ajalt.mordant.table
 
 import com.github.ajalt.mordant.rendering.BorderStyle
 import com.github.ajalt.mordant.rendering.OverflowWrap
@@ -131,6 +131,6 @@ class TableColumnWidthTest : RenderingTest() {
                     cells(11, "22 foo", 33, 44)
                 }
             }
-        }, expected.trimIndent(), width = tableWidth)
+        }, expected.trimIndent(), width = tableWidth, trimMargin = false)
     }
 }
diff --git a/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/rendering/table/TableTest.kt b/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/table/TableTest.kt
similarity index 85%
rename from mordant/src/commonTest/kotlin/com/github/ajalt/mordant/rendering/table/TableTest.kt
rename to mordant/src/commonTest/kotlin/com/github/ajalt/mordant/table/TableTest.kt
index 14dcec58..4c83e751 100644
--- a/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/rendering/table/TableTest.kt
+++ b/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/table/TableTest.kt
@@ -1,4 +1,4 @@
-package com.github.ajalt.mordant.rendering.table
+package com.github.ajalt.mordant.table
 
 import com.github.ajalt.mordant.rendering.*
 import com.github.ajalt.mordant.rendering.TextAlign.CENTER
@@ -6,11 +6,11 @@ import com.github.ajalt.mordant.rendering.TextColors.blue
 import com.github.ajalt.mordant.rendering.TextColors.red
 import com.github.ajalt.mordant.rendering.VerticalAlign.MIDDLE
 import com.github.ajalt.mordant.rendering.Whitespace.PRE
-import com.github.ajalt.mordant.table.*
 import com.github.ajalt.mordant.table.Borders.*
 import com.github.ajalt.mordant.terminal.Terminal
 import com.github.ajalt.mordant.test.RenderingTest
 import com.github.ajalt.mordant.widgets.Padding
+import com.github.ajalt.mordant.widgets.ProgressBar
 import com.github.ajalt.mordant.widgets.Text
 import kotlin.js.JsName
 import kotlin.test.Test
@@ -439,19 +439,85 @@ class TableTest : RenderingTest() {
             row(1, ".2.", 3)
             row(4, 5, 6)
         }, """
-    1   .2. 3
-    4   5   6
-    """
+        |1   .2. 3
+        |4   5   6
+        """
     )
 
+    @Test
+    fun row() = checkRender(
+        row {
+            column(1) { width = ColumnWidth.Expand() }
+            val bar = ProgressBar(indeterminate = true, showPulse = false, indeterminateStyle = TextStyle())
+            cells(1, bar, 2)
+        }, """
+        |1 ━━━━━━ 2⏎
+        """, width = 10)
+
+    @Test
+    fun rowPadding() = checkRender(
+        row(padding = 2) {
+            column(1) { width = ColumnWidth.Expand() }
+            val bar = ProgressBar(indeterminate = true, showPulse = false, indeterminateStyle = TextStyle())
+            cells(1, bar, 2)
+        }, """
+        |1  ━━━━  2⏎
+        """, width = 10)
+
+    @Test
+    fun column() = checkRender(
+        column {
+            cell("1111")
+            cells("2", "3") { align = TextAlign.RIGHT }
+            cellsFrom(listOf(Text("4 4")))
+        }, """
+        |1111⏎
+        |   2⏎
+        |   3⏎
+        |4 4 ⏎
+        """)
+
+    @Test
+    fun columnPadding() = checkRender(
+        column(padding = 1) {
+            cell("1")
+            cell("2")
+            cell("3") { padding = Padding.of(top = 2) }
+            cell("4") { padding = Padding.of(top = 0) }
+            cell("5")
+
+        }, """
+        |1⏎
+        | ⏎
+        |2⏎
+        | ⏎
+        | ⏎
+        |3⏎
+        |4⏎
+        | ⏎
+        |5⏎
+        """)
+
+    @Test
+    @JsName("nesting_rows_in_columns")
+    fun `nesting rows in columns`() = checkRender(
+        column {
+            cell(row { cells("1", "1") })
+            cell(row { cells("222", "2") })
+
+        }, """
+        |1 1  ⏎
+        |222 2⏎
+        """)
+
     private fun doTest(expected: String, builder: TableBuilder.() -> Unit) {
-        checkRender(table(builder), expected.trimMargin(), trimIndent = false)
+        checkRender(table(builder), expected)
     }
 
     private fun doBodyTest(expected: String, builder: SectionBuilder.() -> Unit) {
         checkRender(table {
             borderStyle = BorderStyle.HEAVY_HEAD_FOOT
             body(builder)
-        }, expected.trimMargin(), trimIndent = false)
+        }, expected)
     }
 }
diff --git a/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/test/RenderingTest.kt b/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/test/RenderingTest.kt
index f2197d4b..bae1943c 100644
--- a/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/test/RenderingTest.kt
+++ b/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/test/RenderingTest.kt
@@ -1,6 +1,5 @@
 package com.github.ajalt.mordant.test
 
-import com.github.ajalt.mordant.internal.CSI
 import com.github.ajalt.mordant.rendering.AnsiLevel
 import com.github.ajalt.mordant.rendering.Theme
 import com.github.ajalt.mordant.rendering.Widget
@@ -13,7 +12,7 @@ abstract class RenderingTest(
     protected fun checkRender(
         widget: Widget,
         expected: String,
-        trimIndent: Boolean = true,
+        trimMargin: Boolean = true,
         width: Int = this.width,
         height: Int = 24,
         tabWidth: Int = 8,
@@ -24,7 +23,7 @@ abstract class RenderingTest(
         val t = Terminal(AnsiLevel.TRUECOLOR, theme, width, height, hyperlinks, tabWidth)
         val actual = transformActual(t.render(widget))
         try {
-            val trimmed = if (trimIndent) expected.trimIndent() else expected
+            val trimmed = if (trimMargin) expected.trimMargin() else expected
             actual shouldBe trimmed.replace("⏎", "")
         } catch (e: Throwable) {
             println(actual)
diff --git a/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/rendering/components/DefinitionListTest.kt b/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/widgets/DefinitionListTest.kt
similarity index 72%
rename from mordant/src/commonTest/kotlin/com/github/ajalt/mordant/rendering/components/DefinitionListTest.kt
rename to mordant/src/commonTest/kotlin/com/github/ajalt/mordant/widgets/DefinitionListTest.kt
index 3c5e1ef9..7f06d7aa 100644
--- a/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/rendering/components/DefinitionListTest.kt
+++ b/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/widgets/DefinitionListTest.kt
@@ -1,4 +1,4 @@
-package com.github.ajalt.mordant.rendering.components
+package com.github.ajalt.mordant.widgets
 
 import com.github.ajalt.mordant.rendering.Whitespace.NORMAL
 import com.github.ajalt.mordant.rendering.Whitespace.PRE
@@ -15,8 +15,8 @@ class DefinitionListTest : RenderingTest() {
     @Test
     @JsName("multiple_items_inline")
     fun `multiple items inline`() = doTest("""
-term 1:      desc 1
-term 2 2:    desc 2 2
+    |term 1:      desc 1
+    |term 2 2:    desc 2 2
     """) {
         entry("term 1:", "desc 1")
         entry("term 2 2:", "desc 2 2")
@@ -27,11 +27,11 @@ term 2 2:    desc 2 2
     @Test
     @JsName("inline_desc_wrap")
     fun `inline desc wrap`() = doTest("""
-term 1:    Lorem ipsum
-           dolor sit
-           amet
-term 2 2:  desc 2 2
-   """, width = 23) {
+    |term 1:    Lorem ipsum
+    |           dolor sit
+    |           amet
+    |term 2 2:  desc 2 2
+    """, width = 23) {
         entry("term 1:", Text("Lorem ipsum dolor sit amet", whitespace = NORMAL))
         entry("term 2 2:", "desc 2 2")
         inline = true
@@ -40,9 +40,9 @@ term 2 2:  desc 2 2
     @Test
     @JsName("inline_desc_no_wrap_due_to_short_desc")
     fun `inline desc no wrap due to short desc`() = doTest("""
-term 1 lorem ipsum:  1
-term 2 2:            2
-   """, width = 23) {
+    |term 1 lorem ipsum:  1
+    |term 2 2:            2
+    """, width = 23) {
         entry("term 1 lorem ipsum:", "1")
         entry("term 2 2:", "2")
         inline = true
@@ -51,11 +51,11 @@ term 2 2:            2
     @Test
     @JsName("inline_term_overflow")
     fun `inline term overflow`() = doTest("""
-term 1:   desc 1
-term 2 lorem ipsum:
-          dolor sit amet
-term 3::  desc 3
-   """, width = 24) {
+    |term 1:   desc 1
+    |term 2 lorem ipsum:
+    |          dolor sit amet
+    |term 3::  desc 3
+    """, width = 24) {
         entry("term 1:", "desc 1")
         entry("term 2 lorem ipsum:", "dolor sit amet")
         entry("term 3::", "desc 3")
@@ -65,11 +65,11 @@ term 3::  desc 3
     @Test
     @JsName("inline_term_all_overflow")
     fun `inline term all overflow`() = doTest("""
-term 1 lorem ipsum:
-    dolor sit amet
-term 2 lorem ipsum:
-    dolor sit amet
-   """, width = 24) {
+    |term 1 lorem ipsum:
+    |    dolor sit amet
+    |term 2 lorem ipsum:
+    |    dolor sit amet
+    """, width = 24) {
         inline = true
         entry("term 1 lorem ipsum:", "dolor sit amet")
         entry("term 2 lorem ipsum:", "dolor sit amet")
@@ -78,12 +78,12 @@ term 2 lorem ipsum:
     @Test
     @JsName("inline_term_overflow_wrap")
     fun `inline term overflow wrap`() = doTest("""
-term 1:  desc 1
-term 2 lorem ipsum
-dolor sit amet:
-         desc 2
-term 3:  desc 3
-   """, width = 21) {
+    |term 1:  desc 1
+    |term 2 lorem ipsum
+    |dolor sit amet:
+    |         desc 2
+    |term 3:  desc 3
+    """, width = 21) {
         inline = true
 
         entry("term 1:", "desc 1")
@@ -94,15 +94,15 @@ term 3:  desc 3
     @Test
     @JsName("inline_multi_line_term_and_desc")
     fun `inline multi line term and desc`() = doTest("""
-term 1:   desc 1
-.
-.
-term 2:   desc 2
-          .
-          .
-╭──────╮  ╭──────╮
-│term 3│  │desc 3│
-╰──────╯  ╰──────╯
+   |term 1:   desc 1
+   |.
+   |.
+   |term 2:   desc 2
+   |          .
+   |          .
+   |╭──────╮  ╭──────╮
+   |│term 3│  │desc 3│
+   |╰──────╯  ╰──────╯
    """, width = 21) {
         inline = true
 
@@ -115,17 +115,17 @@ term 2:   desc 2
     @Test
     @JsName("non_inline")
     fun `non-inline`() = doTest("""
-term 1:
-desc 1
-
-term 2 lorem ipsum
-dolor sit amet:
-desc 2
-
-term 3:
-desc 3 consectetur
-adipiscing elit
-""", width = 21) {
+    |term 1:
+    |desc 1
+    |
+    |term 2 lorem ipsum
+    |dolor sit amet:
+    |desc 2
+    |
+    |term 3:
+    |desc 3 consectetur
+    |adipiscing elit
+    """, width = 21) {
         entry("term 1:", "desc 1")
         entry {
             term(Text("term 2 lorem ipsum dolor sit amet:", whitespace = NORMAL))
@@ -140,16 +140,16 @@ adipiscing elit
     @Test
     @JsName("non_inline_custom_spacing")
     fun `non-inline custom spacing`() = doTest("""
-term 1:
-
-desc 1
-term 2:
-
-desc 2
-term 3:
-
-desc 3
-   """, width = 21) {
+    |term 1:
+    |
+    |desc 1
+    |term 2:
+    |
+    |desc 2
+    |term 3:
+    |
+    |desc 3
+    """, width = 21) {
         entrySpacing = 0
         descriptionSpacing = 1
         entry("term 1:", "desc 1")
diff --git a/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/rendering/components/HorizontalRuleTest.kt b/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/widgets/HorizontalRuleTest.kt
similarity index 94%
rename from mordant/src/commonTest/kotlin/com/github/ajalt/mordant/rendering/components/HorizontalRuleTest.kt
rename to mordant/src/commonTest/kotlin/com/github/ajalt/mordant/widgets/HorizontalRuleTest.kt
index 710fdf21..17e70e1f 100644
--- a/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/rendering/components/HorizontalRuleTest.kt
+++ b/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/widgets/HorizontalRuleTest.kt
@@ -1,4 +1,4 @@
-package com.github.ajalt.mordant.rendering.components
+package com.github.ajalt.mordant.widgets
 
 import com.github.ajalt.mordant.rendering.TextAlign
 import com.github.ajalt.mordant.rendering.TextColors.blue
@@ -48,8 +48,8 @@ class HorizontalRuleTest : RenderingTest() {
         checkRender(
             HorizontalRule(title = Text("Multiline\nHeader Text", whitespace = Whitespace.PRE_WRAP)),
             """
-                 Multiline     ⏎
-            ─── Header Text ───⏎
+            |     Multiline     ⏎
+            |─── Header Text ───⏎
             """,
             width = 19
         )
diff --git a/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/widgets/PaddingTest.kt b/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/widgets/PaddingTest.kt
new file mode 100644
index 00000000..e06e3b06
--- /dev/null
+++ b/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/widgets/PaddingTest.kt
@@ -0,0 +1,70 @@
+package com.github.ajalt.mordant.widgets
+
+import com.github.ajalt.mordant.test.RenderingTest
+import kotlin.test.Test
+
+class PaddingTest : RenderingTest() {
+    private val w = Text("x")
+
+    @Test
+    fun oneValue() = checkRender(w.withPadding(1), """
+    |⏎
+    | x ⏎
+    |⏎
+    """)
+
+    @Test
+    fun twoValues() = checkRender(w.withPadding(1, 2), """
+    |⏎
+    |  x  ⏎
+    |⏎
+    """)
+
+    @Test
+    fun threeValues() = checkRender(w.withPadding(1, 2, 3), """
+    |⏎
+    |  x  ⏎
+    |⏎
+    |⏎
+    |⏎
+    """)
+
+    @Test
+    fun fourValues() = checkRender(w.withPadding(1, 2, 3, 4), """
+    |⏎
+    |    x  ⏎
+    |⏎
+    |⏎
+    |⏎
+    """)
+
+    @Test
+    fun vertical() = checkRender(w.withVerticalPadding(1), """
+    |⏎
+    |x⏎
+    |⏎
+    """)
+
+    @Test
+    fun horizontal() = checkRender(w.withHorizontalPadding(1), """
+    | x ⏎
+    """)
+
+    @Test
+    fun padEmpty() = checkRender(Text("x\n\ny").withPadding(1, padEmptyLines = true), """
+    |⏎
+    | x ⏎
+    |  ⏎
+    | y ⏎
+    |⏎
+    """)
+
+    @Test
+    fun noPadEmpty() = checkRender(Text("x\n\ny").withPadding(1, padEmptyLines = false), """
+    |⏎
+    | x ⏎
+    |⏎
+    | y ⏎
+    |⏎
+    """)
+}
diff --git a/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/rendering/components/PanelTest.kt b/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/widgets/PanelTest.kt
similarity index 63%
rename from mordant/src/commonTest/kotlin/com/github/ajalt/mordant/rendering/components/PanelTest.kt
rename to mordant/src/commonTest/kotlin/com/github/ajalt/mordant/widgets/PanelTest.kt
index bb9dc016..af769424 100644
--- a/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/rendering/components/PanelTest.kt
+++ b/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/widgets/PanelTest.kt
@@ -1,12 +1,10 @@
-package com.github.ajalt.mordant.rendering.components
+package com.github.ajalt.mordant.widgets
 
 import com.github.ajalt.mordant.rendering.TextAlign
 import com.github.ajalt.mordant.rendering.TextColors.*
 import com.github.ajalt.mordant.rendering.Theme
 import com.github.ajalt.mordant.rendering.Whitespace.PRE
 import com.github.ajalt.mordant.test.RenderingTest
-import com.github.ajalt.mordant.widgets.Panel
-import com.github.ajalt.mordant.widgets.Text
 import kotlin.js.JsName
 import kotlin.test.Test
 
@@ -17,9 +15,9 @@ class PanelTest : RenderingTest(width = 20) {
     fun `no expand`() = checkRender(
         Panel(Text("text"), expand = false),
         """
-        ╭────╮
-        │text│
-        ╰────╯
+        |╭────╮
+        |│text│
+        |╰────╯
         """
     )
 
@@ -27,9 +25,9 @@ class PanelTest : RenderingTest(width = 20) {
     fun expand() = checkRender(
         Panel(Text("text", align = TextAlign.CENTER), expand = true),
         """
-        ╭──────────────────╮
-        │       text       │
-        ╰──────────────────╯
+        |╭──────────────────╮
+        |│       text       │
+        |╰──────────────────╯
         """
     )
 
@@ -40,7 +38,7 @@ class PanelTest : RenderingTest(width = 20) {
         """
         |text  ⏎
         |line 2⏎
-        """.trimMargin()
+        """
     )
 
     @Test
@@ -48,9 +46,9 @@ class PanelTest : RenderingTest(width = 20) {
     fun `default title`() = checkRender(
         Panel("text content", title = "title"),
         """
-        ╭── title ───╮
-        │text content│
-        ╰────────────╯
+        |╭── title ───╮
+        |│text content│
+        |╰────────────╯
         """
     )
 
@@ -59,9 +57,9 @@ class PanelTest : RenderingTest(width = 20) {
     fun `long title`() = checkRender(
         Panel("content", title = "title title"),
         """
-        ╭ title title ╮
-        │content      │
-        ╰─────────────╯
+        |╭ title title ╮
+        |│content      │
+        |╰─────────────╯
         """
     )
 
@@ -70,9 +68,9 @@ class PanelTest : RenderingTest(width = 20) {
     fun `title align left`() = checkRender(
         Panel("text content", title = "title", titleAlign = TextAlign.LEFT),
         """
-        ╭─ title ────╮
-        │text content│
-        ╰────────────╯
+        |╭─ title ────╮
+        |│text content│
+        |╰────────────╯
         """
     )
 
@@ -81,9 +79,9 @@ class PanelTest : RenderingTest(width = 20) {
     fun `title align right`() = checkRender(
         Panel("text content", title = "title", titleAlign = TextAlign.RIGHT),
         """
-        ╭──── title ─╮
-        │text content│
-        ╰────────────╯
+        |╭──── title ─╮
+        |│text content│
+        |╰────────────╯
         """
     )
 
@@ -92,13 +90,13 @@ class PanelTest : RenderingTest(width = 20) {
     fun `themed panel`() = checkRender(
         Panel(green("text content"), title = blue("title")),
         """
-        ${red("╭───${blue("title")}────╮")}
-        ${red("│${green("text content")}│")}
-        ${red("╰────────────╯")}
+        |${red("╭───${blue("title")}────╮")}
+        |${red("│${green("text content")}│")}
+        |${red("╰────────────╯")}
         """,
         theme = Theme {
             styles["panel.border"] = red
             dimensions["panel.title.padding"] = 0
-        }
+        },
     )
 }
diff --git a/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/widgets/ProgressBarTest.kt b/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/widgets/ProgressBarTest.kt
index 73d42e02..b27c9ba1 100644
--- a/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/widgets/ProgressBarTest.kt
+++ b/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/widgets/ProgressBarTest.kt
@@ -100,7 +100,7 @@ class ProgressBarTest : RenderingTest() {
             expected,
             theme = theme,
             width = 5,
-            trimIndent = false,
+            trimMargin = false,
         )
     }
 }
diff --git a/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/widgets/SpinnerTest.kt b/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/widgets/SpinnerTest.kt
index 2325dd93..5693ef79 100644
--- a/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/widgets/SpinnerTest.kt
+++ b/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/widgets/SpinnerTest.kt
@@ -29,7 +29,7 @@ class SpinnerTest : RenderingTest() {
         vararg expected: String,
     ) {
         for (ex in expected) {
-            checkRender(spinner, ex)
+            checkRender(spinner, ex, trimMargin = false)
             val t = spinner.tick
             spinner.advanceTick() shouldBe (t + 1)
         }
diff --git a/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/rendering/components/TextTest.kt b/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/widgets/TextTest.kt
similarity index 85%
rename from mordant/src/commonTest/kotlin/com/github/ajalt/mordant/rendering/components/TextTest.kt
rename to mordant/src/commonTest/kotlin/com/github/ajalt/mordant/widgets/TextTest.kt
index c7e9da69..695ce4cf 100644
--- a/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/rendering/components/TextTest.kt
+++ b/mordant/src/commonTest/kotlin/com/github/ajalt/mordant/widgets/TextTest.kt
@@ -1,4 +1,4 @@
-package com.github.ajalt.mordant.rendering.components
+package com.github.ajalt.mordant.widgets
 
 import com.github.ajalt.colormath.model.Ansi256
 import com.github.ajalt.colormath.model.RGB
@@ -12,9 +12,6 @@ import com.github.ajalt.mordant.rendering.TextColors.*
 import com.github.ajalt.mordant.rendering.Whitespace.PRE
 import com.github.ajalt.mordant.test.RenderingTest
 import com.github.ajalt.mordant.test.normalizeHyperlinks
-import com.github.ajalt.mordant.widgets.LS
-import com.github.ajalt.mordant.widgets.NEL
-import com.github.ajalt.mordant.widgets.Text
 import io.kotest.data.blocking.forAll
 import io.kotest.data.row
 import kotlin.js.JsName
@@ -28,9 +25,9 @@ class TextTest : RenderingTest() {
     Lorem ipsum dolor
     sit amet
     """, whitespace = Whitespace.NORMAL, width = 12), """
-    Lorem ipsum
-    dolor sit
-    amet
+    |Lorem ipsum
+    |dolor sit
+    |amet
     """, width = 79)
 
     @Test
@@ -39,10 +36,10 @@ class TextTest : RenderingTest() {
     Lorem${NEL}ipsum dolor $LS
     sit $LS  amet
     """, whitespace = Whitespace.NORMAL), """
-    Lorem
-    ipsum dolor
-    sit
-    amet
+    |Lorem
+    |ipsum dolor
+    |sit
+    |amet
     """)
 
     @Test
@@ -53,7 +50,7 @@ class TextTest : RenderingTest() {
         row("...\t.",/* */"... ."),
         row("....\t.",/**/"....    ."),
     ) { text, expected ->
-        checkRender(Text(text, whitespace = PRE), expected, tabWidth = 4, trimIndent = false)
+        checkRender(Text(text, whitespace = PRE), expected, tabWidth = 4)
     }
 
     @Test
@@ -63,9 +60,9 @@ class TextTest : RenderingTest() {
     ${CSI}255munknown
     ${CSI}6ndevice status report
     """.trimIndent(), whitespace = PRE), """
-    ${CSI}31;47mred ${CSI}34mblue ${CSI}100mgray.bg${CSI}31;47m red${CSI}39;49m
-    unknown
-    device status report
+    |${CSI}31;47mred ${CSI}34mblue ${CSI}100mgray.bg${CSI}31;47m red${CSI}39;49m
+    |unknown
+    |device status report
     """, width = 79)
 
     @Test
@@ -88,8 +85,8 @@ class TextTest : RenderingTest() {
     ${TextStyle(RGB(1, 0, 0), white)("red ${TextStyle(blue)("blue ${TextStyle(bgColor = gray)("gray.bg")}")} red")}
     ${TextStyle(hyperlink = "foo.com")("foo.${TextStyle(hyperlink = "bar.com")("bar")}.com")}/baz
     """.trimIndent(), whitespace = PRE), """
-    ${CSI}38;2;255;0;0;47mred ${CSI}34mblue ${CSI}100mgray.bg${CSI}38;2;255;0;0;47m red${CSI}39;49m
-    ${OSC}8;id=1;foo.com${ST}foo.${OSC}8;id=2;bar.com${ST}bar${OSC}8;id=1;foo.com$ST.com${OSC}8;;$ST/baz
+    |${CSI}38;2;255;0;0;47mred ${CSI}34mblue ${CSI}100mgray.bg${CSI}38;2;255;0;0;47m red${CSI}39;49m
+    |${OSC}8;id=1;foo.com${ST}foo.${OSC}8;id=2;bar.com${ST}bar${OSC}8;id=1;foo.com$ST.com${OSC}8;;$ST/baz
     """, width = 79) { it.normalizeHyperlinks() }
 
     @Test
@@ -130,7 +127,8 @@ class TextTest : RenderingTest() {
             whitespace = Whitespace.NORMAL,
             overflowWrap = overflowWrap
         ),
-        expected,
-        width = width
+        expected.trimIndent(),
+        width = width,
+        trimMargin = false,
     ) { it.normalizeHyperlinks() }
 }