Skip to content

Commit

Permalink
Decorated window with custom titlebar support (#173)
Browse files Browse the repository at this point in the history
* Decorated window support

* TitleBar for different platform

* TitleBar on windows

* Avoid use custom title bar on linux

* Linux custom title bar support

* Add control buttons to linux

* Support window border on Linux

* Support window border on Linux

* Fix linux border

* Support double click to maxilize window on linux

* Make detekt happy

* Make detekt happy

* Make detekt happy

* Make detekt happy

* Remove unused local window

* Make detekt happy

* Make ktlint happy

* Remove unused JBR component

* Make tooltip placement same as IJ

* Fix comments

* Fix comments

* Switch themes with titlebar icon

* Avoid pass lightWithLightHeader to withDecoratedWindow DSL

* Dropdown in titlebar support

* Dropdown in titlebar support

* Fix comment

* Make ktlint happy

* Fix comments

* Fix comments
  • Loading branch information
devkanro authored Oct 17, 2023
1 parent 4da6105 commit 4fbfde0
Show file tree
Hide file tree
Showing 85 changed files with 3,410 additions and 127 deletions.
145 changes: 70 additions & 75 deletions core/src/main/kotlin/org/jetbrains/jewel/Dropdown.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusManager
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.input.InputMode
import androidx.compose.ui.input.InputModeManager
import androidx.compose.ui.platform.LocalDensity
Expand Down Expand Up @@ -60,91 +61,85 @@ fun Dropdown(
menuContent: MenuScope.() -> Unit,
content: @Composable BoxScope.() -> Unit,
) {
Box {
var expanded by remember { mutableStateOf(false) }
var skipNextClick by remember { mutableStateOf(false) }
var expanded by remember { mutableStateOf(false) }
var skipNextClick by remember { mutableStateOf(false) }

var dropdownState by remember(interactionSource) {
mutableStateOf(DropdownState.of(enabled = enabled))
}
var dropdownState by remember(interactionSource) {
mutableStateOf(DropdownState.of(enabled = enabled))
}

remember(enabled) {
dropdownState = dropdownState.copy(enabled = enabled)
}
remember(enabled) {
dropdownState = dropdownState.copy(enabled = enabled)
}

LaunchedEffect(interactionSource) {
interactionSource.interactions.collect { interaction ->
when (interaction) {
is PressInteraction.Press -> dropdownState = dropdownState.copy(pressed = true)
is PressInteraction.Cancel, is PressInteraction.Release ->
dropdownState =
dropdownState.copy(pressed = false)
LaunchedEffect(interactionSource) {
interactionSource.interactions.collect { interaction ->
when (interaction) {
is PressInteraction.Press -> dropdownState = dropdownState.copy(pressed = true)
is PressInteraction.Cancel, is PressInteraction.Release ->
dropdownState =
dropdownState.copy(pressed = false)

is HoverInteraction.Enter -> dropdownState = dropdownState.copy(hovered = true)
is HoverInteraction.Exit -> dropdownState = dropdownState.copy(hovered = false)
is FocusInteraction.Focus -> dropdownState = dropdownState.copy(focused = true)
is FocusInteraction.Unfocus -> dropdownState = dropdownState.copy(focused = false)
}
is HoverInteraction.Enter -> dropdownState = dropdownState.copy(hovered = true)
is HoverInteraction.Exit -> dropdownState = dropdownState.copy(hovered = false)
is FocusInteraction.Focus -> dropdownState = dropdownState.copy(focused = true)
is FocusInteraction.Unfocus -> dropdownState = dropdownState.copy(focused = false)
}
}
}

val colors = style.colors
val metrics = style.metrics
val shape = RoundedCornerShape(style.metrics.cornerSize)
val minSize = metrics.minSize
val arrowMinSize = style.metrics.arrowMinSize
val borderColor by colors.borderFor(dropdownState)

val outlineState = remember(dropdownState, expanded) {
dropdownState.copy(focused = dropdownState.isFocused || expanded)
}
val colors = style.colors
val metrics = style.metrics
val shape = RoundedCornerShape(style.metrics.cornerSize)
val minSize = metrics.minSize
val arrowMinSize = style.metrics.arrowMinSize
val borderColor by colors.borderFor(dropdownState)

Box(
modifier.clickable(
onClick = {
// TODO: Trick to skip click event when close menu by click dropdown
if (!skipNextClick) {
expanded = !expanded
}
skipNextClick = false
},
enabled = enabled,
role = Role.Button,
interactionSource = interactionSource,
indication = null,
)
.background(colors.backgroundFor(dropdownState).value, shape)
.border(Stroke.Alignment.Center, style.metrics.borderWidth, borderColor, shape)
.appendIf(outline == Outline.None) { focusOutline(outlineState, shape) }
.outline(outlineState, outline, shape)
.width(IntrinsicSize.Max)
.defaultMinSize(minSize.width, minSize.height.coerceAtLeast(arrowMinSize.height)),
contentAlignment = Alignment.CenterStart,
Box(
modifier.clickable(
onClick = {
// TODO: Trick to skip click event when close menu by click dropdown
if (!skipNextClick) {
expanded = !expanded
}
skipNextClick = false
},
enabled = enabled,
role = Role.Button,
interactionSource = interactionSource,
indication = null,
)
.background(colors.backgroundFor(dropdownState).value, shape)
.border(Stroke.Alignment.Center, style.metrics.borderWidth, borderColor, shape)
.appendIf(outline == Outline.None) { focusOutline(dropdownState, shape) }
.outline(dropdownState, outline, shape)
.width(IntrinsicSize.Max)
.defaultMinSize(minSize.width, minSize.height.coerceAtLeast(arrowMinSize.height)),
contentAlignment = Alignment.CenterStart,
) {
CompositionLocalProvider(
LocalContentColor provides colors.contentFor(dropdownState).value,
) {
CompositionLocalProvider(
LocalContentColor provides colors.contentFor(dropdownState).value,
Box(
modifier = Modifier
.fillMaxWidth()
.padding(style.metrics.contentPadding)
.padding(end = minSize.height),
contentAlignment = Alignment.CenterStart,
content = content,
)

Box(
modifier = Modifier.size(arrowMinSize)
.align(Alignment.CenterEnd),
contentAlignment = Alignment.Center,
) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(style.metrics.contentPadding)
.padding(end = minSize.height),
contentAlignment = Alignment.CenterStart,
content = content,
val chevronIcon by style.icons.chevronDown.getPainter(resourceLoader, dropdownState)
Icon(
painter = chevronIcon,
contentDescription = null,
tint = colors.iconTintFor(dropdownState).value,
)

Box(
modifier = Modifier.size(arrowMinSize)
.align(Alignment.CenterEnd),
contentAlignment = Alignment.Center,
) {
val chevronIcon by style.icons.chevronDown.getPainter(resourceLoader, dropdownState)
Icon(
painter = chevronIcon,
contentDescription = null,
tint = colors.iconTintFor(dropdownState).value,
)
}
}
}

Expand All @@ -157,7 +152,7 @@ fun Dropdown(
}
true
},
modifier = menuModifier,
modifier = menuModifier.focusProperties { canFocus = true },
style = style.menuStyle,
horizontalAlignment = Alignment.Start,
content = menuContent,
Expand Down
4 changes: 3 additions & 1 deletion core/src/main/kotlin/org/jetbrains/jewel/IconButton.kt
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,9 @@ fun IconButton(
.border(style.metrics.borderWidth, border, shape),
contentAlignment = Alignment.Center,
content = {
content(buttonState)
onBackground(background) {
content(buttonState)
}
},
)
}
26 changes: 26 additions & 0 deletions core/src/main/kotlin/org/jetbrains/jewel/IntelliJTheme.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.isSpecified
import androidx.compose.ui.text.TextStyle
import org.jetbrains.jewel.styling.ButtonStyle
import org.jetbrains.jewel.styling.CheckboxStyle
Expand Down Expand Up @@ -46,6 +47,7 @@ import org.jetbrains.jewel.styling.TabStyle
import org.jetbrains.jewel.styling.TextAreaStyle
import org.jetbrains.jewel.styling.TextFieldStyle
import org.jetbrains.jewel.styling.TooltipStyle
import org.jetbrains.jewel.util.isDark

interface IntelliJTheme {

Expand Down Expand Up @@ -220,10 +222,12 @@ fun IntelliJTheme(
fun IntelliJTheme(theme: IntelliJThemeDefinition, content: @Composable () -> Unit) {
CompositionLocalProvider(
LocalIsDarkTheme provides theme.isDark,
LocalOnDarkBackground provides theme.isDark,
LocalContentColor provides theme.contentColor,
LocalTextStyle provides theme.defaultTextStyle,
LocalGlobalColors provides theme.globalColors,
LocalGlobalMetrics provides theme.globalMetrics,
*theme.extensionStyles,
content = content,
)
}
Expand All @@ -232,6 +236,10 @@ internal val LocalIsDarkTheme = staticCompositionLocalOf<Boolean> {
error("No InDarkTheme provided")
}

internal val LocalOnDarkBackground = staticCompositionLocalOf<Boolean> {
error("No OnDarkBackground provided")
}

internal val LocalSwingCompatMode = staticCompositionLocalOf {
// By default, Swing compat is not enabled
false
Expand All @@ -244,3 +252,21 @@ val LocalColorPalette = staticCompositionLocalOf<IntelliJThemeColorPalette> {
val LocalIconData = staticCompositionLocalOf<IntelliJThemeIconData> {
EmptyThemeIconData
}

/**
* Sets the background color of the current area,
* which affects the style(light or dark) of the icon rendered above it,
* by calculating the luminance.
* If the color is not specified, the style will follow the current theme style.
* Transparent color will be ignored.
*/
@Composable
fun onBackground(color: Color, content: @Composable () -> Unit) {
val locals = if (color.isSpecified && color.alpha > 0) {
arrayOf(LocalOnDarkBackground provides color.isDark())
} else {
emptyArray()
}

CompositionLocalProvider(values = locals, content)
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.jetbrains.jewel

import androidx.compose.runtime.Immutable
import androidx.compose.runtime.ProvidedValue
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle

Expand All @@ -15,4 +16,8 @@ interface IntelliJThemeDefinition {

val colorPalette: IntelliJThemeColorPalette
val iconData: IntelliJThemeIconData

val extensionStyles: Array<ProvidedValue<*>>

fun withExtensions(vararg extensions: ProvidedValue<*>): IntelliJThemeDefinition
}
4 changes: 2 additions & 2 deletions core/src/main/kotlin/org/jetbrains/jewel/Menu.kt
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@ fun PopupMenu(
density = density,
)

var focusManager: FocusManager? by mutableStateOf(null)
var inputModeManager: InputModeManager? by mutableStateOf(null)
var focusManager: FocusManager? by remember { mutableStateOf(null) }
var inputModeManager: InputModeManager? by remember { mutableStateOf(null) }
val menuManager = remember(onDismissRequest) {
MenuManager(onDismissRequest = onDismissRequest)
}
Expand Down
Loading

0 comments on commit 4fbfde0

Please sign in to comment.