From 2077d68fdfbafd8b14c4b561123847659cd822fb Mon Sep 17 00:00:00 2001 From: fscarponi Date: Mon, 18 Dec 2023 16:28:53 +0100 Subject: [PATCH] Add custom tab styles and refine tab data structures This update implements support for custom tab styles in the Jewel UI theme. The changes encompass refinement of the TabData class and the introduction of Custom, ToolWindowTab, and Editor tab styles. These modifications aim at enhancing customization and versatility particularly for tab labels. --- .../samples/standalone/view/component/Tabs.kt | 62 ++++++++++++++++--- .../jetbrains/jewel/ui/component/TabStrip.kt | 41 ++++++++---- .../org/jetbrains/jewel/ui/component/Tabs.kt | 34 ++++++---- .../jewel/ui/component/styling/TabStyling.kt | 5 ++ .../jetbrains/jewel/ui/theme/JewelTheme.kt | 8 ++- 5 files changed, 118 insertions(+), 32 deletions(-) diff --git a/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/component/Tabs.kt b/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/component/Tabs.kt index 8fd2105ed6..141cf0dbc8 100644 --- a/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/component/Tabs.kt +++ b/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/component/Tabs.kt @@ -14,6 +14,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import org.jetbrains.jewel.foundation.theme.JewelTheme import org.jetbrains.jewel.samples.standalone.StandaloneSampleIcons @@ -23,22 +24,69 @@ import org.jetbrains.jewel.ui.component.IconButton import org.jetbrains.jewel.ui.component.TabData import org.jetbrains.jewel.ui.component.TabStrip import org.jetbrains.jewel.ui.component.Text -import org.jetbrains.jewel.ui.theme.defaultTabStyle +import org.jetbrains.jewel.ui.theme.editorTabStyle import kotlin.math.max @Composable @View(title = "Tabs", position = 7) fun Tabs() { - Text("Default tabs", Modifier.fillMaxWidth()) - DefaultTabShowcase() + Text("Toolwindow tabs", Modifier.fillMaxWidth()) + ToolwindowTabShowcase() Spacer(Modifier.height(16.dp)) Text("Editor tabs", Modifier.fillMaxWidth()) EditorTabShowcase() + + Spacer(Modifier.height(16.dp)) + Text("Custom tabs", Modifier.fillMaxWidth()) + CustomTabShowcase() +} + +@Composable +fun CustomTabShowcase() { + var selectedTabIndex by remember { mutableStateOf(0) } + + var tabIds by remember { mutableStateOf((1..12).toList()) } + val maxId = remember(tabIds) { tabIds.maxOrNull() ?: 0 } + + val tabs = remember(tabIds, selectedTabIndex) { + tabIds.mapIndexed { index, id -> + TabData.Custom( + selected = index == selectedTabIndex, + content = { + val textColor = when { + it.isHovered -> Color.Red + else -> Color.Unspecified + + } + Text(text = "Custom tab $id", color = textColor) + + }, + onClose = { + tabIds = tabIds.toMutableList().apply { removeAt(index) } + if (selectedTabIndex >= index) { + val maxPossibleIndex = max(0, tabIds.lastIndex) + selectedTabIndex = (selectedTabIndex - 1) + .coerceIn(0..maxPossibleIndex) + } + }, + onClick = { selectedTabIndex = index }, + ) + } + } + + TabStripWithAddButton(tabs) { + val insertionIndex = (selectedTabIndex + 1).coerceIn(0..tabIds.size) + val nextTabId = maxId + 1 + + tabIds = tabIds.toMutableList() + .apply { add(insertionIndex, nextTabId) } + selectedTabIndex = insertionIndex + } } @Composable -private fun DefaultTabShowcase() { +private fun ToolwindowTabShowcase() { var selectedTabIndex by remember { mutableStateOf(0) } var tabIds by remember { mutableStateOf((1..12).toList()) } @@ -46,7 +94,7 @@ private fun DefaultTabShowcase() { val tabs = remember(tabIds, selectedTabIndex) { tabIds.mapIndexed { index, id -> - TabData.Default( + TabData.Default.ToolWindowTab( selected = index == selectedTabIndex, label = "Default tab $id", onClose = { @@ -81,7 +129,7 @@ private fun EditorTabShowcase() { val tabs = remember(tabIds, selectedTabIndex) { tabIds.mapIndexed { index, id -> - TabData.Editor( + TabData.Default.Editor( selected = index == selectedTabIndex, label = "Editor tab $id", onClose = { @@ -117,7 +165,7 @@ private fun TabStripWithAddButton( IconButton( onClick = onAddClick, - modifier = Modifier.size(JewelTheme.defaultTabStyle.metrics.tabHeight), + modifier = Modifier.size(JewelTheme.editorTabStyle.metrics.tabHeight), ) { Icon( resource = "expui/general/add.svg", diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/TabStrip.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/TabStrip.kt index 5b37353139..6306d59ac6 100644 --- a/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/TabStrip.kt +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/TabStrip.kt @@ -30,6 +30,7 @@ import org.jetbrains.jewel.foundation.GenerateDataFunctions import org.jetbrains.jewel.foundation.modifier.onHover import org.jetbrains.jewel.foundation.state.CommonStateBitMask import org.jetbrains.jewel.foundation.state.FocusableComponentState +import org.jetbrains.jewel.foundation.state.InteractiveComponentState @Composable public fun TabStrip( @@ -81,28 +82,44 @@ public fun TabStrip( public sealed class TabData { public abstract val selected: Boolean - public abstract val label: String public abstract val icon: Painter? public abstract val closable: Boolean public abstract val onClose: () -> Unit public abstract val onClick: () -> Unit @Immutable - @GenerateDataFunctions - public class Default( - override val selected: Boolean, - override val label: String, - override val icon: Painter? = null, - override val closable: Boolean = true, - override val onClose: () -> Unit = {}, - override val onClick: () -> Unit = {}, - ) : TabData() + public sealed class Default : TabData() { + + public abstract val label: String + + @Immutable + @GenerateDataFunctions + public class ToolWindowTab( + override val selected: Boolean, + override val label: String, + override val icon: Painter? = null, + override val closable: Boolean = true, + override val onClose: () -> Unit = {}, + override val onClick: () -> Unit = {}, + ) : Default() + + @Immutable + @GenerateDataFunctions + public class Editor( + override val selected: Boolean, + override val label: String, + override val icon: Painter? = null, + override val closable: Boolean = true, + override val onClose: () -> Unit = {}, + override val onClick: () -> Unit = {}, + ) : Default() + } @Immutable @GenerateDataFunctions - public class Editor( + public class Custom( override val selected: Boolean, - override val label: String, + public val content: @Composable (tabState: InteractiveComponentState) -> Unit, override val icon: Painter? = null, override val closable: Boolean = true, override val onClose: () -> Unit = {}, diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Tabs.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Tabs.kt index 41df851fea..fb50dd9b33 100644 --- a/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Tabs.kt +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Tabs.kt @@ -45,8 +45,9 @@ import org.jetbrains.jewel.foundation.theme.JewelTheme import org.jetbrains.jewel.foundation.theme.LocalContentColor import org.jetbrains.jewel.ui.NoIndication import org.jetbrains.jewel.ui.painter.hints.Stateful -import org.jetbrains.jewel.ui.theme.defaultTabStyle +import org.jetbrains.jewel.ui.theme.customTabStyle import org.jetbrains.jewel.ui.theme.editorTabStyle +import org.jetbrains.jewel.ui.theme.toolWindowTabStyle @Composable internal fun TabImpl( @@ -57,8 +58,9 @@ internal fun TabImpl( ) { val tabStyle = when (tabData) { - is TabData.Default -> JewelTheme.defaultTabStyle - is TabData.Editor -> JewelTheme.editorTabStyle + is TabData.Default.ToolWindowTab -> JewelTheme.toolWindowTabStyle + is TabData.Default.Editor -> JewelTheme.editorTabStyle + is TabData.Custom -> JewelTheme.customTabStyle } var tabState by remember { @@ -127,17 +129,25 @@ internal fun TabImpl( Image(modifier = Modifier.alpha(iconAlpha), painter = icon, contentDescription = null) } - Text( - modifier = Modifier.alpha(labelAlpha), - text = tabData.label, - color = tabStyle.colors.contentFor(tabState).value, - ) + when (tabData) { + is TabData.Custom -> { + tabData.content(tabState) + } + + is TabData.Default -> { + Text( + modifier = Modifier.alpha(labelAlpha), + text = tabData.label, + color = tabStyle.colors.contentFor(tabState).value, + ) + } + } + val showCloseIcon = when (tabData) { - is TabData.Default -> tabData.closable - is TabData.Editor -> tabData.closable && (tabState.isHovered || tabState.isSelected) + is TabData.Default.ToolWindowTab -> tabData.closable + is TabData.Default.Editor, is TabData.Custom -> tabData.closable && (tabState.isHovered || tabState.isSelected) } - if (showCloseIcon) { val closeActionInteractionSource = remember { MutableInteractionSource() } LaunchedEffect(closeActionInteractionSource) { @@ -166,7 +176,7 @@ internal fun TabImpl( ) .size(16.dp), painter = closePainter, - contentDescription = "Close tab ${tabData.label}", + contentDescription = "Close tab", ) } else if (tabData.closable) { Spacer(Modifier.size(16.dp)) diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/styling/TabStyling.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/styling/TabStyling.kt index 67a5a8d9b3..95cf32f7bf 100644 --- a/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/styling/TabStyling.kt +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/styling/TabStyling.kt @@ -189,3 +189,8 @@ public val LocalEditorTabStyle: ProvidableCompositionLocal = staticCompositionLocalOf { error("No LocalTabStyle provided. Have you forgotten the theme?") } + +public val LocalCustomTabStyle: ProvidableCompositionLocal = + staticCompositionLocalOf { + error("No LocalTabStyle provided. Have you forgotten the theme?") + } diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/theme/JewelTheme.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/theme/JewelTheme.kt index 4b3f65997f..ef4d887997 100644 --- a/ui/src/main/kotlin/org/jetbrains/jewel/ui/theme/JewelTheme.kt +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/theme/JewelTheme.kt @@ -26,6 +26,7 @@ import org.jetbrains.jewel.ui.component.styling.LinkStyle import org.jetbrains.jewel.ui.component.styling.LocalCheckboxStyle import org.jetbrains.jewel.ui.component.styling.LocalChipStyle import org.jetbrains.jewel.ui.component.styling.LocalCircularProgressStyle +import org.jetbrains.jewel.ui.component.styling.LocalCustomTabStyle import org.jetbrains.jewel.ui.component.styling.LocalDefaultButtonStyle import org.jetbrains.jewel.ui.component.styling.LocalDefaultDropdownStyle import org.jetbrains.jewel.ui.component.styling.LocalDefaultTabStyle @@ -142,7 +143,7 @@ public val JewelTheme.Companion.treeStyle: LazyTreeStyle @ReadOnlyComposable get() = LocalLazyTreeStyle.current -public val JewelTheme.Companion.defaultTabStyle: TabStyle +public val JewelTheme.Companion.toolWindowTabStyle: TabStyle @Composable @ReadOnlyComposable get() = LocalDefaultTabStyle.current @@ -152,6 +153,11 @@ public val JewelTheme.Companion.editorTabStyle: TabStyle @ReadOnlyComposable get() = LocalEditorTabStyle.current +public val JewelTheme.Companion.customTabStyle: TabStyle + @Composable + @ReadOnlyComposable + get() = LocalCustomTabStyle.current + public val JewelTheme.Companion.circularProgressStyle: CircularProgressStyle @Composable @ReadOnlyComposable