From 4fbfde02102db9e4e63149bd28bdb6ac90642b00 Mon Sep 17 00:00:00 2001 From: Kanro Date: Tue, 17 Oct 2023 16:46:22 +0800 Subject: [PATCH] Decorated window with custom titlebar support (#173) * 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 --- .../kotlin/org/jetbrains/jewel/Dropdown.kt | 145 +++-- .../kotlin/org/jetbrains/jewel/IconButton.kt | 4 +- .../org/jetbrains/jewel/IntelliJTheme.kt | 26 + .../jewel/IntelliJThemeDefinition.kt | 5 + .../main/kotlin/org/jetbrains/jewel/Menu.kt | 4 +- .../kotlin/org/jetbrains/jewel/Tooltip.kt | 119 +++- .../jewel/styling/DropdownStyling.kt | 16 +- .../styling/SimpleResourcePathPatcher.kt | 4 +- .../jetbrains/jewel/styling/TooltipStyling.kt | 5 + .../jetbrains/jewel/util/ColorExtensions.kt | 3 + decorated-window/build.gradle.kts | 16 + .../java/com/jetbrains/DesktopActions.java | 42 ++ .../src/main/java/com/jetbrains/JBR.java | 220 +++++++ .../com/jetbrains/RoundedCornersManager.java | 46 ++ .../java/com/jetbrains/WindowDecorations.java | 177 ++++++ .../main/java/com/jetbrains/WindowMove.java | 53 ++ .../jetbrains/jewel/window/DecoratedWindow.kt | 280 +++++++++ .../org/jetbrains/jewel/window/Theme.kt | 19 + .../jetbrains/jewel/window/TitleBar.Linux.kt | 101 ++++ .../jetbrains/jewel/window/TitleBar.MacOS.kt | 105 ++++ .../jewel/window/TitleBar.Windows.kt | 63 ++ .../org/jetbrains/jewel/window/TitleBar.kt | 297 +++++++++ .../window/styling/DecoratedWindowStyling.kt | 40 ++ .../jewel/window/styling/TitleBarStyling.kt | 91 +++ .../jewel/window/utils/DesktopPlatform.kt | 21 + .../jetbrains/jewel/window/utils/JnaLoader.kt | 43 ++ .../jewel/window/utils/UnsafeAccessing.kt | 84 +++ .../jewel/window/utils/macos/Foundation.kt | 98 +++ .../window/utils/macos/FoundationLibrary.kt | 89 +++ .../jetbrains/jewel/window/utils/macos/ID.kt | 26 + .../jewel/window/utils/macos/MacUtil.kt | 95 +++ gradle/libs.versions.toml | 3 + .../jewel/intui/core/IntUiThemeDefinition.kt | 33 +- .../int-ui-decorated-window/build.gradle.kts | 10 + .../jewel/intui/window/IntUiTheme.kt | 32 + .../styling/IntUiDecoratedWindowStyling.kt | 118 ++++ .../window/styling/IntUiTitleBarStyling.kt | 566 ++++++++++++++++++ .../resources/icons/intui/window/close.svg | 5 + .../icons/intui/window/closeActive.svg | 7 + .../icons/intui/window/closeInactive.svg | 5 + .../icons/intui/window/closeInactive_dark.svg | 5 + .../icons/intui/window/close_dark.svg | 5 + .../resources/icons/intui/window/maximize.svg | 4 + .../icons/intui/window/maximizeInactive.svg | 4 + .../intui/window/maximizeInactive_dark.svg | 4 + .../icons/intui/window/maximize_dark.svg | 4 + .../resources/icons/intui/window/minimize.svg | 4 + .../icons/intui/window/minimizeInactive.svg | 4 + .../intui/window/minimizeInactive_dark.svg | 4 + .../icons/intui/window/minimize_dark.svg | 4 + .../resources/icons/intui/window/restore.svg | 5 + .../icons/intui/window/restoreInactive.svg | 5 + .../intui/window/restoreInactive_dark.svg | 5 + .../icons/intui/window/restore_dark.svg | 5 + .../styling/IntUiDropdownStyling.kt | 43 +- .../standalone/styling/IntUiMenuStyling.kt | 2 +- .../standalone/styling/IntUiTooltipStyling.kt | 4 + samples/standalone/build.gradle.kts | 1 + .../jewel/samples/standalone/IntUiThemes.kt | 9 + .../jewel/samples/standalone/Main.kt | 148 ++++- .../src/main/resources/icons/cwmAccess.svg | 6 + .../main/resources/icons/cwmAccess@20x20.svg | 6 + .../resources/icons/cwmAccess@20x20_dark.svg | 6 + .../main/resources/icons/cwmAccess_dark.svg | 6 + .../src/main/resources/icons/darkTheme.svg | 3 + .../main/resources/icons/darkTheme@20x20.svg | 3 + .../resources/icons/darkTheme@20x20_dark.svg | 3 + .../main/resources/icons/darkTheme_dark.svg | 3 + .../src/main/resources/icons/github.svg | 5 + .../src/main/resources/icons/github@20x20.svg | 5 + .../resources/icons/github@20x20_dark.svg | 5 + .../src/main/resources/icons/github_dark.svg | 5 + .../src/main/resources/icons/lightTheme.svg | 18 + .../main/resources/icons/lightTheme@20x20.svg | 11 + .../resources/icons/lightTheme@20x20_dark.svg | 11 + .../main/resources/icons/lightTheme_dark.svg | 18 + .../src/main/resources/icons/search.svg | 7 +- .../src/main/resources/icons/search@20x20.svg | 4 + .../resources/icons/search@20x20_dark.svg | 4 + .../src/main/resources/icons/search_dark.svg | 5 + .../src/main/resources/icons/settings.svg | 4 + .../main/resources/icons/settings@20x20.svg | 4 + .../resources/icons/settings@20x20_dark.svg | 4 + .../main/resources/icons/settings_dark.svg | 4 + settings.gradle.kts | 2 + 85 files changed, 3410 insertions(+), 127 deletions(-) create mode 100644 decorated-window/build.gradle.kts create mode 100644 decorated-window/src/main/java/com/jetbrains/DesktopActions.java create mode 100644 decorated-window/src/main/java/com/jetbrains/JBR.java create mode 100644 decorated-window/src/main/java/com/jetbrains/RoundedCornersManager.java create mode 100644 decorated-window/src/main/java/com/jetbrains/WindowDecorations.java create mode 100644 decorated-window/src/main/java/com/jetbrains/WindowMove.java create mode 100644 decorated-window/src/main/kotlin/org/jetbrains/jewel/window/DecoratedWindow.kt create mode 100644 decorated-window/src/main/kotlin/org/jetbrains/jewel/window/Theme.kt create mode 100644 decorated-window/src/main/kotlin/org/jetbrains/jewel/window/TitleBar.Linux.kt create mode 100644 decorated-window/src/main/kotlin/org/jetbrains/jewel/window/TitleBar.MacOS.kt create mode 100644 decorated-window/src/main/kotlin/org/jetbrains/jewel/window/TitleBar.Windows.kt create mode 100644 decorated-window/src/main/kotlin/org/jetbrains/jewel/window/TitleBar.kt create mode 100644 decorated-window/src/main/kotlin/org/jetbrains/jewel/window/styling/DecoratedWindowStyling.kt create mode 100644 decorated-window/src/main/kotlin/org/jetbrains/jewel/window/styling/TitleBarStyling.kt create mode 100644 decorated-window/src/main/kotlin/org/jetbrains/jewel/window/utils/DesktopPlatform.kt create mode 100644 decorated-window/src/main/kotlin/org/jetbrains/jewel/window/utils/JnaLoader.kt create mode 100644 decorated-window/src/main/kotlin/org/jetbrains/jewel/window/utils/UnsafeAccessing.kt create mode 100644 decorated-window/src/main/kotlin/org/jetbrains/jewel/window/utils/macos/Foundation.kt create mode 100644 decorated-window/src/main/kotlin/org/jetbrains/jewel/window/utils/macos/FoundationLibrary.kt create mode 100644 decorated-window/src/main/kotlin/org/jetbrains/jewel/window/utils/macos/ID.kt create mode 100644 decorated-window/src/main/kotlin/org/jetbrains/jewel/window/utils/macos/MacUtil.kt create mode 100644 int-ui/int-ui-decorated-window/build.gradle.kts create mode 100644 int-ui/int-ui-decorated-window/src/main/kotlin/org/jetbrains/jewel/intui/window/IntUiTheme.kt create mode 100644 int-ui/int-ui-decorated-window/src/main/kotlin/org/jetbrains/jewel/intui/window/styling/IntUiDecoratedWindowStyling.kt create mode 100644 int-ui/int-ui-decorated-window/src/main/kotlin/org/jetbrains/jewel/intui/window/styling/IntUiTitleBarStyling.kt create mode 100644 int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/close.svg create mode 100644 int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/closeActive.svg create mode 100644 int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/closeInactive.svg create mode 100644 int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/closeInactive_dark.svg create mode 100644 int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/close_dark.svg create mode 100644 int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/maximize.svg create mode 100644 int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/maximizeInactive.svg create mode 100644 int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/maximizeInactive_dark.svg create mode 100644 int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/maximize_dark.svg create mode 100644 int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/minimize.svg create mode 100644 int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/minimizeInactive.svg create mode 100644 int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/minimizeInactive_dark.svg create mode 100644 int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/minimize_dark.svg create mode 100644 int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/restore.svg create mode 100644 int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/restoreInactive.svg create mode 100644 int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/restoreInactive_dark.svg create mode 100644 int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/restore_dark.svg create mode 100644 samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/IntUiThemes.kt create mode 100644 samples/standalone/src/main/resources/icons/cwmAccess.svg create mode 100644 samples/standalone/src/main/resources/icons/cwmAccess@20x20.svg create mode 100644 samples/standalone/src/main/resources/icons/cwmAccess@20x20_dark.svg create mode 100644 samples/standalone/src/main/resources/icons/cwmAccess_dark.svg create mode 100644 samples/standalone/src/main/resources/icons/darkTheme.svg create mode 100644 samples/standalone/src/main/resources/icons/darkTheme@20x20.svg create mode 100644 samples/standalone/src/main/resources/icons/darkTheme@20x20_dark.svg create mode 100644 samples/standalone/src/main/resources/icons/darkTheme_dark.svg create mode 100644 samples/standalone/src/main/resources/icons/github.svg create mode 100644 samples/standalone/src/main/resources/icons/github@20x20.svg create mode 100644 samples/standalone/src/main/resources/icons/github@20x20_dark.svg create mode 100644 samples/standalone/src/main/resources/icons/github_dark.svg create mode 100644 samples/standalone/src/main/resources/icons/lightTheme.svg create mode 100644 samples/standalone/src/main/resources/icons/lightTheme@20x20.svg create mode 100644 samples/standalone/src/main/resources/icons/lightTheme@20x20_dark.svg create mode 100644 samples/standalone/src/main/resources/icons/lightTheme_dark.svg create mode 100644 samples/standalone/src/main/resources/icons/search@20x20.svg create mode 100644 samples/standalone/src/main/resources/icons/search@20x20_dark.svg create mode 100644 samples/standalone/src/main/resources/icons/search_dark.svg create mode 100644 samples/standalone/src/main/resources/icons/settings.svg create mode 100644 samples/standalone/src/main/resources/icons/settings@20x20.svg create mode 100644 samples/standalone/src/main/resources/icons/settings@20x20_dark.svg create mode 100644 samples/standalone/src/main/resources/icons/settings_dark.svg diff --git a/core/src/main/kotlin/org/jetbrains/jewel/Dropdown.kt b/core/src/main/kotlin/org/jetbrains/jewel/Dropdown.kt index d9c5dbf7b..8b16a780d 100644 --- a/core/src/main/kotlin/org/jetbrains/jewel/Dropdown.kt +++ b/core/src/main/kotlin/org/jetbrains/jewel/Dropdown.kt @@ -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 @@ -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, - ) - } } } @@ -157,7 +152,7 @@ fun Dropdown( } true }, - modifier = menuModifier, + modifier = menuModifier.focusProperties { canFocus = true }, style = style.menuStyle, horizontalAlignment = Alignment.Start, content = menuContent, diff --git a/core/src/main/kotlin/org/jetbrains/jewel/IconButton.kt b/core/src/main/kotlin/org/jetbrains/jewel/IconButton.kt index 61b4e4a2f..f610e44ac 100644 --- a/core/src/main/kotlin/org/jetbrains/jewel/IconButton.kt +++ b/core/src/main/kotlin/org/jetbrains/jewel/IconButton.kt @@ -74,7 +74,9 @@ fun IconButton( .border(style.metrics.borderWidth, border, shape), contentAlignment = Alignment.Center, content = { - content(buttonState) + onBackground(background) { + content(buttonState) + } }, ) } diff --git a/core/src/main/kotlin/org/jetbrains/jewel/IntelliJTheme.kt b/core/src/main/kotlin/org/jetbrains/jewel/IntelliJTheme.kt index 9b5472886..697947ecb 100644 --- a/core/src/main/kotlin/org/jetbrains/jewel/IntelliJTheme.kt +++ b/core/src/main/kotlin/org/jetbrains/jewel/IntelliJTheme.kt @@ -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 @@ -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 { @@ -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, ) } @@ -232,6 +236,10 @@ internal val LocalIsDarkTheme = staticCompositionLocalOf { error("No InDarkTheme provided") } +internal val LocalOnDarkBackground = staticCompositionLocalOf { + error("No OnDarkBackground provided") +} + internal val LocalSwingCompatMode = staticCompositionLocalOf { // By default, Swing compat is not enabled false @@ -244,3 +252,21 @@ val LocalColorPalette = staticCompositionLocalOf { val LocalIconData = staticCompositionLocalOf { 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) +} diff --git a/core/src/main/kotlin/org/jetbrains/jewel/IntelliJThemeDefinition.kt b/core/src/main/kotlin/org/jetbrains/jewel/IntelliJThemeDefinition.kt index 31ba5d9f4..133d8dcae 100644 --- a/core/src/main/kotlin/org/jetbrains/jewel/IntelliJThemeDefinition.kt +++ b/core/src/main/kotlin/org/jetbrains/jewel/IntelliJThemeDefinition.kt @@ -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 @@ -15,4 +16,8 @@ interface IntelliJThemeDefinition { val colorPalette: IntelliJThemeColorPalette val iconData: IntelliJThemeIconData + + val extensionStyles: Array> + + fun withExtensions(vararg extensions: ProvidedValue<*>): IntelliJThemeDefinition } diff --git a/core/src/main/kotlin/org/jetbrains/jewel/Menu.kt b/core/src/main/kotlin/org/jetbrains/jewel/Menu.kt index 733cb202d..cba4e67e2 100644 --- a/core/src/main/kotlin/org/jetbrains/jewel/Menu.kt +++ b/core/src/main/kotlin/org/jetbrains/jewel/Menu.kt @@ -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) } diff --git a/core/src/main/kotlin/org/jetbrains/jewel/Tooltip.kt b/core/src/main/kotlin/org/jetbrains/jewel/Tooltip.kt index b98bade98..f504a5e63 100644 --- a/core/src/main/kotlin/org/jetbrains/jewel/Tooltip.kt +++ b/core/src/main/kotlin/org/jetbrains/jewel/Tooltip.kt @@ -9,23 +9,34 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.shadow +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntRect +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.PopupPositionProvider import org.jetbrains.jewel.styling.TooltipStyle -@Composable fun Tooltip( +@Composable +fun Tooltip( tooltip: @Composable () -> Unit, modifier: Modifier = Modifier, - tooltipPlacement: TooltipPlacement = TooltipPlacement.ComponentRect( - alignment = Alignment.CenterEnd, - anchor = Alignment.BottomEnd, - offset = DpOffset(4.dp, 4.dp), - ), style: TooltipStyle = IntelliJTheme.tooltipStyle, + tooltipPlacement: TooltipPlacement = IntellijTooltipPlacement( + contentOffset = style.metrics.tooltipOffset, + alignment = style.metrics.tooltipAlignment, + density = LocalDensity.current, + ), content: @Composable () -> Unit, ) { TooltipArea( @@ -52,7 +63,9 @@ import org.jetbrains.jewel.styling.TooltipStyle ) .padding(style.metrics.contentPadding), ) { - tooltip() + onBackground(style.colors.background) { + tooltip() + } } } }, @@ -62,3 +75,95 @@ import org.jetbrains.jewel.styling.TooltipStyle content = content, ) } + +class IntellijTooltipPlacement( + private val contentOffset: DpOffset, + private val alignment: Alignment.Horizontal, + private val density: Density, + private val windowMargin: Dp = 4.dp, +) : TooltipPlacement { + + @Composable + @Suppress("OVERRIDE_DEPRECATION") + override fun positionProvider(): PopupPositionProvider { + error("Not supported") + } + + @Composable + override fun positionProvider(cursorPosition: Offset): PopupPositionProvider { + return rememberIntellijTooltipPositionProvider( + cursorPosition = cursorPosition, + contentOffset = contentOffset, + alignment = alignment, + density = density, + windowMargin = windowMargin, + ) + } +} + +@Composable +private fun rememberIntellijTooltipPositionProvider( + cursorPosition: Offset, + contentOffset: DpOffset, + alignment: Alignment.Horizontal, + density: Density, + windowMargin: Dp = 4.dp, +) = remember( + contentOffset, + alignment, + density, + windowMargin, +) { + IntellijTooltipPositionProvider( + cursorPosition = cursorPosition, + contentOffset = contentOffset, + alignment = alignment, + density = density, + windowMargin = windowMargin, + ) +} + +private class IntellijTooltipPositionProvider( + private val cursorPosition: Offset, + private val contentOffset: DpOffset, + private val alignment: Alignment.Horizontal, + private val density: Density, + private val windowMargin: Dp = 4.dp, +) : PopupPositionProvider { + + override fun calculatePosition( + anchorBounds: IntRect, + windowSize: IntSize, + layoutDirection: LayoutDirection, + popupContentSize: IntSize, + ): IntOffset = with(density) { + val windowSpaceBounds = IntRect( + left = windowMargin.roundToPx(), + top = windowMargin.roundToPx(), + right = windowSize.width - windowMargin.roundToPx(), + bottom = windowSize.height - windowMargin.roundToPx(), + ) + + val contentOffsetX = contentOffset.x.roundToPx() + val contentOffsetY = contentOffset.y.roundToPx() + + val posX = cursorPosition.x.toInt() + anchorBounds.left + val posY = cursorPosition.y.toInt() + anchorBounds.top + + val x = posX + alignment.align(popupContentSize.width, 0, layoutDirection) + contentOffsetX + + val aboveSpacing = cursorPosition.y - contentOffsetY - windowSpaceBounds.top + val belowSpacing = windowSpaceBounds.bottom - cursorPosition.y - contentOffsetY + + val y = if (belowSpacing > popupContentSize.height || belowSpacing >= aboveSpacing) { + posY + contentOffsetY + } else { + posY - contentOffsetY - popupContentSize.height + } + + val popupBounds = IntRect(x, y, x + popupContentSize.width, y + popupContentSize.height) + .constrainedIn(windowSpaceBounds) + + IntOffset(popupBounds.left, popupBounds.top) + } +} diff --git a/core/src/main/kotlin/org/jetbrains/jewel/styling/DropdownStyling.kt b/core/src/main/kotlin/org/jetbrains/jewel/styling/DropdownStyling.kt index 647998bca..2604fcfab 100644 --- a/core/src/main/kotlin/org/jetbrains/jewel/styling/DropdownStyling.kt +++ b/core/src/main/kotlin/org/jetbrains/jewel/styling/DropdownStyling.kt @@ -34,14 +34,14 @@ interface DropdownColors { @Composable fun backgroundFor(state: DropdownState) = rememberUpdatedState( - state.chooseValue( - normal = background, - disabled = backgroundDisabled, - focused = backgroundFocused, - pressed = backgroundPressed, - hovered = backgroundHovered, - active = background, - ), + when { + !state.isEnabled -> backgroundDisabled + state.isPressed -> backgroundPressed + state.isHovered -> backgroundHovered + state.isFocused -> backgroundFocused + state.isActive -> background + else -> background + }, ) val content: Color diff --git a/core/src/main/kotlin/org/jetbrains/jewel/styling/SimpleResourcePathPatcher.kt b/core/src/main/kotlin/org/jetbrains/jewel/styling/SimpleResourcePathPatcher.kt index 072f81164..2575a62ac 100644 --- a/core/src/main/kotlin/org/jetbrains/jewel/styling/SimpleResourcePathPatcher.kt +++ b/core/src/main/kotlin/org/jetbrains/jewel/styling/SimpleResourcePathPatcher.kt @@ -2,7 +2,7 @@ package org.jetbrains.jewel.styling import androidx.compose.runtime.Composable import androidx.compose.ui.res.ResourceLoader -import org.jetbrains.jewel.LocalIsDarkTheme +import org.jetbrains.jewel.LocalOnDarkBackground open class SimpleResourcePathPatcher : ResourcePathPatcher { @@ -28,7 +28,7 @@ open class SimpleResourcePathPatcher : ResourcePathPatcher { // TODO load HiDPI rasterized images ("@2x") // TODO load sized SVG images (e.g., "@20x20") - if (LocalIsDarkTheme.current) { + if (LocalOnDarkBackground.current) { append("_dark") } diff --git a/core/src/main/kotlin/org/jetbrains/jewel/styling/TooltipStyling.kt b/core/src/main/kotlin/org/jetbrains/jewel/styling/TooltipStyling.kt index bc9bada87..e4c2efebe 100644 --- a/core/src/main/kotlin/org/jetbrains/jewel/styling/TooltipStyling.kt +++ b/core/src/main/kotlin/org/jetbrains/jewel/styling/TooltipStyling.kt @@ -4,8 +4,10 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.shape.CornerSize import androidx.compose.runtime.Stable import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.Alignment import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpOffset import kotlin.time.Duration @Stable @@ -32,6 +34,9 @@ interface TooltipMetrics { val cornerSize: CornerSize val borderWidth: Dp val shadowSize: Dp + + val tooltipOffset: DpOffset + val tooltipAlignment: Alignment.Horizontal } val LocalTooltipStyle = staticCompositionLocalOf { diff --git a/core/src/main/kotlin/org/jetbrains/jewel/util/ColorExtensions.kt b/core/src/main/kotlin/org/jetbrains/jewel/util/ColorExtensions.kt index f2da077e4..1f847f407 100644 --- a/core/src/main/kotlin/org/jetbrains/jewel/util/ColorExtensions.kt +++ b/core/src/main/kotlin/org/jetbrains/jewel/util/ColorExtensions.kt @@ -1,6 +1,7 @@ package org.jetbrains.jewel.util import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.luminance import kotlin.math.roundToInt fun Color.toHexString(): String { @@ -20,3 +21,5 @@ fun Color.toHexString(): String { } } } + +fun Color.isDark(): Boolean = (luminance() + 0.05) / 0.05 < 4.5 diff --git a/decorated-window/build.gradle.kts b/decorated-window/build.gradle.kts new file mode 100644 index 000000000..577c600d3 --- /dev/null +++ b/decorated-window/build.gradle.kts @@ -0,0 +1,16 @@ +import org.jetbrains.compose.ComposeBuildConfig + +plugins { + jewel + `jewel-publish` + alias(libs.plugins.composeDesktop) + alias(libs.plugins.kotlinSerialization) +} + +private val composeVersion get() = ComposeBuildConfig.composeVersion + +dependencies { + api("org.jetbrains.compose.foundation:foundation-desktop:$composeVersion") + api(projects.core) + implementation(libs.jna.core) +} diff --git a/decorated-window/src/main/java/com/jetbrains/DesktopActions.java b/decorated-window/src/main/java/com/jetbrains/DesktopActions.java new file mode 100644 index 000000000..f2efcb477 --- /dev/null +++ b/decorated-window/src/main/java/com/jetbrains/DesktopActions.java @@ -0,0 +1,42 @@ +/* + * Copyright 2000-2022 JetBrains s.r.o. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.jetbrains; + +import java.io.File; +import java.io.IOException; +import java.net.URI; + +public interface DesktopActions { + + void setHandler(Handler handler); + + interface Handler { + default void open(File file) throws IOException { throw new UnsupportedOperationException(); } + default void edit(File file) throws IOException { throw new UnsupportedOperationException(); } + default void print(File file) throws IOException { throw new UnsupportedOperationException(); } + default void mail(URI mailtoURL) throws IOException { throw new UnsupportedOperationException(); } + default void browse(URI uri) throws IOException { throw new UnsupportedOperationException(); } + } + +} diff --git a/decorated-window/src/main/java/com/jetbrains/JBR.java b/decorated-window/src/main/java/com/jetbrains/JBR.java new file mode 100644 index 000000000..0f5f04505 --- /dev/null +++ b/decorated-window/src/main/java/com/jetbrains/JBR.java @@ -0,0 +1,220 @@ +/* + * Copyright 2000-2023 JetBrains s.r.o. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.jetbrains; + +import java.lang.invoke.MethodHandles; +import java.lang.reflect.InvocationTargetException; + +/** + * This class is an entry point into JBR API. + * JBR API is a collection of services, classes, interfaces, etc., + * which require tight interaction with JRE and therefore are implemented inside JBR. + *
JBR API consists of two parts:
+ *
    + *
  • Client side - {@code jetbrains.api} module, mostly containing interfaces
  • + *
  • JBR side - actual implementation code inside JBR
  • + *
+ * Client and JBR side are linked dynamically at runtime and do not have to be of the same version. + * In some cases (e.g. running on different JRE or old JBR) system will not be able to find + * implementation for some services, so you'll need a fallback behavior for that case. + *

Simple usage example:

+ *
{@code
+ * if (JBR.isSomeServiceSupported()) {
+ *     JBR.getSomeService().doSomething();
+ * } else {
+ *     planB();
+ * }
+ * }
+ * + * @implNote JBR API is initialized on first access to this class (in static initializer). + * Actual implementation is linked on demand, when corresponding service is requested by client. + */ +public class JBR { + + private static final ServiceApi api; + private static final Exception bootstrapException; + + static { + ServiceApi a = null; + Exception exception = null; + try { + a = (ServiceApi) Class.forName("com.jetbrains.bootstrap.JBRApiBootstrap") + .getMethod("bootstrap", MethodHandles.Lookup.class) + .invoke(null, MethodHandles.lookup()); + } catch (InvocationTargetException e) { + Throwable t = e.getCause(); + if (t instanceof Error error) throw error; + else throw new Error(t); + } catch (IllegalAccessException | NoSuchMethodException | ClassNotFoundException e) { + exception = e; + } + api = a; + bootstrapException = exception; + } + + private JBR() { + } + + private static T getService(Class interFace, FallbackSupplier fallback) { + T service = getService(interFace); + try { + return service != null ? service : fallback != null ? fallback.get() : null; + } catch (Throwable ignore) { + return null; + } + } + + static T getService(Class interFace) { + return api == null ? null : api.getService(interFace); + } + + /** + * @return true when running on JBR which implements JBR API + */ + public static boolean isAvailable() { + return api != null; + } + + /** + * @return JBR API version in form {@code JBR.MAJOR.MINOR.PATCH} + * @implNote This is an API version, which comes with client application, + * it has nothing to do with JRE it runs on. + */ + public static String getApiVersion() { + return "17.0.8.1b1070.2.1.9.0"; + } + + /** + * Internal API interface, contains most basic methods for communication between client and JBR. + */ + private interface ServiceApi { + + T getService(Class interFace); + } + + @FunctionalInterface + private interface FallbackSupplier { + T get() throws Throwable; + } + + // ========================== Generated metadata ========================== + + /** + * Generated client-side metadata, needed by JBR when linking the implementation. + */ + private static final class Metadata { + private static final String[] KNOWN_SERVICES = {"com.jetbrains.ExtendedGlyphCache", "com.jetbrains.DesktopActions", "com.jetbrains.CustomWindowDecoration", "com.jetbrains.ProjectorUtils", "com.jetbrains.FontExtensions", "com.jetbrains.RoundedCornersManager", "com.jetbrains.GraphicsUtils", "com.jetbrains.WindowDecorations", "com.jetbrains.JBRFileDialogService", "com.jetbrains.AccessibleAnnouncer", "com.jetbrains.JBR$ServiceApi", "com.jetbrains.Jstack", "com.jetbrains.WindowMove"}; + private static final String[] KNOWN_PROXIES = {"com.jetbrains.JBRFileDialog", "com.jetbrains.WindowDecorations$CustomTitleBar"}; + } + + // ======================= Generated static methods ======================= + + private static class DesktopActions__Holder { + private static final DesktopActions INSTANCE = getService(DesktopActions.class, null); + } + + /** + * @return true if current runtime has implementation for all methods in {@link DesktopActions} + * and its dependencies (can fully implement given service). + * @see #getDesktopActions() + */ + public static boolean isDesktopActionsSupported() { + return DesktopActions__Holder.INSTANCE != null; + } + + /** + * @return full implementation of {@link DesktopActions} service if any, or {@code null} otherwise + */ + public static DesktopActions getDesktopActions() { + return DesktopActions__Holder.INSTANCE; + } + + private static class RoundedCornersManager__Holder { + private static final RoundedCornersManager INSTANCE = getService(RoundedCornersManager.class, null); + } + + /** + * @return true if current runtime has implementation for all methods in {@link RoundedCornersManager} + * and its dependencies (can fully implement given service). + * @see #getRoundedCornersManager() + */ + public static boolean isRoundedCornersManagerSupported() { + return RoundedCornersManager__Holder.INSTANCE != null; + } + + /** + * This manager allows decorate awt Window with rounded corners. + * Appearance depends from operating system. + * + * @return full implementation of {@link RoundedCornersManager} service if any, or {@code null} otherwise + */ + public static RoundedCornersManager getRoundedCornersManager() { + return RoundedCornersManager__Holder.INSTANCE; + } + + private static class WindowDecorations__Holder { + private static final WindowDecorations INSTANCE = getService(WindowDecorations.class, null); + } + + /** + * @return true if current runtime has implementation for all methods in {@link WindowDecorations} + * and its dependencies (can fully implement given service). + * @see #getWindowDecorations() + */ + public static boolean isWindowDecorationsSupported() { + return WindowDecorations__Holder.INSTANCE != null; + } + + /** + * Window decorations consist of title bar, window controls and border. + * + * @return full implementation of {@link WindowDecorations} service if any, or {@code null} otherwise + * @see WindowDecorations.CustomTitleBar + */ + public static WindowDecorations getWindowDecorations() { + return WindowDecorations__Holder.INSTANCE; + } + + private static class WindowMove__Holder { + private static final WindowMove INSTANCE = getService(WindowMove.class, null); + } + + /** + * @return true if current runtime has implementation for all methods in {@link WindowMove} + * and its dependencies (can fully implement given service). + * @see #getWindowMove() + */ + public static boolean isWindowMoveSupported() { + return WindowMove__Holder.INSTANCE != null; + } + + /** + * @return full implementation of {@link WindowMove} service if any, or {@code null} otherwise + */ + public static WindowMove getWindowMove() { + return WindowMove__Holder.INSTANCE; + } +} diff --git a/decorated-window/src/main/java/com/jetbrains/RoundedCornersManager.java b/decorated-window/src/main/java/com/jetbrains/RoundedCornersManager.java new file mode 100644 index 000000000..424e03fed --- /dev/null +++ b/decorated-window/src/main/java/com/jetbrains/RoundedCornersManager.java @@ -0,0 +1,46 @@ +/* + * Copyright 2000-2023 JetBrains s.r.o. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.jetbrains; + +import java.awt.*; + +/** + * This manager allows decorate awt Window with rounded corners. + * Appearance depends from operating system. + */ +public interface RoundedCornersManager { + /** + * @param params for macOS is Float object with radius or + * Array with {Float for radius, Integer for border width, java.awt.Color for border color}. + * + * @param params for Windows 11 is String with values: + * "default" - let the system decide whether or not to round window corners, + * "none" - never round window corners, + * "full" - round the corners if appropriate, + * "small" - round the corners if appropriate, with a small radius. + */ + void setRoundedCorners(Window window, Object params); +} diff --git a/decorated-window/src/main/java/com/jetbrains/WindowDecorations.java b/decorated-window/src/main/java/com/jetbrains/WindowDecorations.java new file mode 100644 index 000000000..fc0144fa7 --- /dev/null +++ b/decorated-window/src/main/java/com/jetbrains/WindowDecorations.java @@ -0,0 +1,177 @@ +/* + * Copyright 2023 JetBrains s.r.o. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.jetbrains; + +import java.awt.*; +import java.util.Map; + +/** + * Window decorations consist of title bar, window controls and border. + * @see CustomTitleBar + */ +public interface WindowDecorations { + + /** + * If {@code customTitleBar} is not null, system-provided title bar is removed and client area is extended to the + * top of the frame with window controls painted over the client area. + * {@code customTitleBar=null} resets to the default appearance with system-provided title bar. + * @see CustomTitleBar + * @see #createCustomTitleBar() + */ + void setCustomTitleBar(Frame frame, CustomTitleBar customTitleBar); + + /** + * If {@code customTitleBar} is not null, system-provided title bar is removed and client area is extended to the + * top of the dialog with window controls painted over the client area. + * {@code customTitleBar=null} resets to the default appearance with system-provided title bar. + * @see CustomTitleBar + * @see #createCustomTitleBar() + */ + void setCustomTitleBar(Dialog dialog, CustomTitleBar customTitleBar); + + /** + * You must {@linkplain CustomTitleBar#setHeight(float) set title bar height} before adding it to a window. + * @see CustomTitleBar + * @see #setCustomTitleBar(Frame, CustomTitleBar) + * @see #setCustomTitleBar(Dialog, CustomTitleBar) + */ + CustomTitleBar createCustomTitleBar(); + + /** + * Custom title bar allows merging of window content with native title bar, + * which is done by treating title bar as part of client area, but with some + * special behavior like dragging or maximizing on double click. + * Custom title bar has {@linkplain CustomTitleBar#getHeight() height} and controls. + * @implNote Behavior is platform-dependent, only macOS and Windows are supported. + * @see #setCustomTitleBar(Frame, CustomTitleBar) + */ + interface CustomTitleBar { + + /** + * @return title bar height, measured in pixels from the top of client area, i.e. excluding top frame border. + */ + float getHeight(); + + /** + * @param height title bar height, measured in pixels from the top of client area, + * i.e. excluding top frame border. Must be > 0. + */ + void setHeight(float height); + + /** + * @see #putProperty(String, Object) + */ + Map getProperties(); + + /** + * @see #putProperty(String, Object) + */ + void putProperties(Map m); + + /** + * Windows & macOS properties: + *
    + *
  • {@code controls.visible} : {@link Boolean} - whether title bar controls + * (minimize/maximize/close buttons) are visible, default = true.
  • + *
+ * Windows properties: + *
    + *
  • {@code controls.width} : {@link Number} - width of block of buttons (not individual buttons). + * Note that dialogs have only one button, while frames usually have 3 of them.
  • + *
  • {@code controls.dark} : {@link Boolean} - whether to use dark or light color theme + * (light or dark icons respectively).
  • + *
  • {@code controls..} : {@link Color} - precise control over button colors, + * where {@code } is one of: + *
    • {@code foreground}
    • {@code background}
    + * and {@code } is one of: + *
      + *
    • {@code normal}
    • + *
    • {@code hovered}
    • + *
    • {@code pressed}
    • + *
    • {@code disabled}
    • + *
    • {@code inactive}
    • + *
    + *
+ */ + void putProperty(String key, Object value); + + /** + * @return space occupied by title bar controls on the left (px) + */ + float getLeftInset(); + /** + * @return space occupied by title bar controls on the right (px) + */ + float getRightInset(); + + /** + * By default, any component which has no cursor or mouse event listeners set is considered transparent for + * native title bar actions. That is, dragging simple JPanel in title bar area will drag the + * window, but dragging a JButton will not. Adding mouse listener to a component will prevent any native actions + * inside bounds of that component. + *

+ * This method gives you precise control of whether to allow native title bar actions or not. + *

    + *
  • {@code client=true} means that mouse is currently over a client area. Native title bar behavior is disabled.
  • + *
  • {@code client=false} means that mouse is currently over a non-client area. Native title bar behavior is enabled.
  • + *
+ * Intended usage: + *
    + *
  • This method must be called in response to all {@linkplain java.awt.event.MouseEvent mouse events} + * except {@link java.awt.event.MouseEvent#MOUSE_EXITED} and {@link java.awt.event.MouseEvent#MOUSE_WHEEL}.
  • + *
  • This method is called per-event, i.e. when component has multiple listeners, you only need to call it once.
  • + *
  • If this method hadn't been called, title bar behavior is reverted back to default upon processing the event.
  • + *
+ * Note that hit test value is relevant only for title bar area, e.g. calling + * {@code forceHitTest(false)} will not make window draggable via non-title bar area. + * + *

Example:

+ * Suppose you have a {@code JPanel} in the title bar area. You want it to respond to right-click for + * some popup menu, but also retain native drag and double-click behavior. + *
+         *     CustomTitleBar titlebar = ...;
+         *     JPanel panel = ...;
+         *     MouseAdapter adapter = new MouseAdapter() {
+         *         private void hit() { titlebar.forceHitTest(false); }
+         *         public void mouseClicked(MouseEvent e) {
+         *             hit();
+         *             if (e.getButton() == MouseEvent.BUTTON3) ...;
+         *         }
+         *         public void mousePressed(MouseEvent e) { hit(); }
+         *         public void mouseReleased(MouseEvent e) { hit(); }
+         *         public void mouseEntered(MouseEvent e) { hit(); }
+         *         public void mouseDragged(MouseEvent e) { hit(); }
+         *         public void mouseMoved(MouseEvent e) { hit(); }
+         *     };
+         *     panel.addMouseListener(adapter);
+         *     panel.addMouseMotionListener(adapter);
+         * 
+ */ + void forceHitTest(boolean client); + + Window getContainingWindow(); + } +} diff --git a/decorated-window/src/main/java/com/jetbrains/WindowMove.java b/decorated-window/src/main/java/com/jetbrains/WindowMove.java new file mode 100644 index 000000000..5f241303c --- /dev/null +++ b/decorated-window/src/main/java/com/jetbrains/WindowMove.java @@ -0,0 +1,53 @@ +/* + * Copyright 2000-2023 JetBrains s.r.o. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.jetbrains; + +import java.awt.*; + +public interface WindowMove { + /** + * Starts moving the top-level parent window of the given window together with the mouse pointer. + * The intended use is to facilitate the implementation of window management similar to the way + * it is done natively on the platform. + * + * Preconditions for calling this method: + *
    + *
  • WM supports _NET_WM_MOVE_RESIZE (this is checked automatically when an implementation + * of this interface is obtained).
  • + *
  • Mouse pointer is within this window's bounds.
  • + *
  • The mouse button specified by {@code mouseButton} is pressed.
  • + *
+ * + * Calling this method will make the window start moving together with the mouse pointer until + * the specified mouse button is released or Esc is pressed. The conditions for cancelling + * the move may differ between WMs. + * + * @param mouseButton indicates the mouse button that was pressed to start moving the window; + * must be one of {@code MouseEvent.BUTTON1}, {@code MouseEvent.BUTTON2}, + * or {@code MouseEvent.BUTTON3}. + */ + void startMovingTogetherWithMouse(Window window, int mouseButton); +} diff --git a/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/DecoratedWindow.kt b/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/DecoratedWindow.kt new file mode 100644 index 000000000..4238a6170 --- /dev/null +++ b/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/DecoratedWindow.kt @@ -0,0 +1,280 @@ +package org.jetbrains.jewel.window + +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.awt.ComposeWindow +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.input.key.KeyEvent +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.Measurable +import androidx.compose.ui.layout.MeasurePolicy +import androidx.compose.ui.layout.MeasureResult +import androidx.compose.ui.layout.MeasureScope +import androidx.compose.ui.layout.Placeable +import androidx.compose.ui.layout.layoutId +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.offset +import androidx.compose.ui.window.FrameWindowScope +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.WindowPlacement +import androidx.compose.ui.window.WindowState +import androidx.compose.ui.window.rememberWindowState +import com.jetbrains.JBR +import org.jetbrains.jewel.IntelliJTheme +import org.jetbrains.jewel.foundation.Stroke +import org.jetbrains.jewel.foundation.border +import org.jetbrains.jewel.window.styling.DecoratedWindowStyle +import org.jetbrains.jewel.window.utils.DesktopPlatform +import java.awt.event.ComponentEvent +import java.awt.event.ComponentListener +import java.awt.event.WindowAdapter +import java.awt.event.WindowEvent + +@Composable fun DecoratedWindow( + onCloseRequest: () -> Unit, + state: WindowState = rememberWindowState(), + visible: Boolean = true, + title: String = "", + icon: Painter? = null, + resizable: Boolean = true, + enabled: Boolean = true, + focusable: Boolean = true, + alwaysOnTop: Boolean = false, + onPreviewKeyEvent: (KeyEvent) -> Boolean = { false }, + onKeyEvent: (KeyEvent) -> Boolean = { false }, + style: DecoratedWindowStyle = IntelliJTheme.defaultDecoratedWindowStyle, + content: @Composable DecoratedWindowScope.() -> Unit, +) { + remember { + if (!JBR.isAvailable()) { + error( + "DecoratedWindow only can be used on JetBrainsRuntime(JBR) platform, " + + "please check the document https://github.com/JetBrains/jewel#int-ui-standalone-theme", + ) + } + } + + // Using undecorated window for linux + val undecorated = DesktopPlatform.Linux == DesktopPlatform.Current + + Window( + onCloseRequest, + state, + visible, + title, + icon, + undecorated, + false, + resizable, + enabled, + focusable, + alwaysOnTop, + onPreviewKeyEvent, + onKeyEvent, + ) { + var decoratedWindowState by remember { mutableStateOf(DecoratedWindowState.of(window)) } + + DisposableEffect(window) { + val adapter = object : WindowAdapter(), ComponentListener { + override fun windowActivated(e: WindowEvent?) { + decoratedWindowState = DecoratedWindowState.of(window) + } + + override fun windowDeactivated(e: WindowEvent?) { + decoratedWindowState = DecoratedWindowState.of(window) + } + + override fun windowIconified(e: WindowEvent?) { + decoratedWindowState = DecoratedWindowState.of(window) + } + + override fun windowDeiconified(e: WindowEvent?) { + decoratedWindowState = DecoratedWindowState.of(window) + } + + override fun windowStateChanged(e: WindowEvent) { + decoratedWindowState = DecoratedWindowState.of(window) + } + + override fun componentResized(e: ComponentEvent?) { + decoratedWindowState = DecoratedWindowState.of(window) + } + + override fun componentMoved(e: ComponentEvent?) { + // Empty + } + + override fun componentShown(e: ComponentEvent?) { + // Empty + } + + override fun componentHidden(e: ComponentEvent?) { + // Empty + } + } + window.addWindowListener(adapter) + window.addWindowStateListener(adapter) + window.addComponentListener(adapter) + onDispose { + window.removeWindowListener(adapter) + window.removeWindowStateListener(adapter) + window.removeComponentListener(adapter) + } + } + + val undecoratedWindowBorder = if (undecorated && !decoratedWindowState.isMaximized) { + Modifier.border( + Stroke.Alignment.Inside, + style.metrics.borderWidth, + style.colors.borderFor(decoratedWindowState).value, + RectangleShape, + ).padding(style.metrics.borderWidth) + } else { + Modifier + } + + CompositionLocalProvider( + LocalTitleBarInfo provides TitleBarInfo(title, icon), + ) { + Layout({ + val scope = object : DecoratedWindowScope { + override val state: DecoratedWindowState + get() = decoratedWindowState + + override val window: ComposeWindow get() = this@Window.window + } + scope.content() + }, modifier = undecoratedWindowBorder, measurePolicy = DecoratedWindowMeasurePolicy) + } + } +} + +@Stable interface DecoratedWindowScope : FrameWindowScope { + + override val window: ComposeWindow + + val state: DecoratedWindowState +} + +private object DecoratedWindowMeasurePolicy : MeasurePolicy { + + override fun MeasureScope.measure(measurables: List, constraints: Constraints): MeasureResult { + if (measurables.isEmpty()) { + return layout( + constraints.minWidth, + constraints.minHeight, + ) {} + } + + val titleBars = measurables.filter { it.layoutId == TITLE_BAR_LAYOUT_ID } + if (titleBars.size > 1) { + error("Window just can have only one title bar") + } + val titleBar = titleBars.firstOrNull() + val titleBarBorder = measurables.firstOrNull { it.layoutId == TITLE_BAR_BORDER_LAYOUT_ID } + + val contentConstraints = constraints.copy(minWidth = 0, minHeight = 0) + + val titleBarPlaceable = titleBar?.measure(contentConstraints) + val titleBarHeight = titleBarPlaceable?.height ?: 0 + + val titleBarBorderPlaceable = titleBarBorder?.measure(contentConstraints) + val titleBarBorderHeight = titleBarBorderPlaceable?.height ?: 0 + + val measuredPlaceable = mutableListOf() + + for (it in measurables) { + if (it.layoutId.toString().startsWith(TITLE_BAR_COMPONENT_LAYOUT_ID_PREFIX)) continue + val placeable = it.measure(contentConstraints.offset(vertical = -titleBarHeight - titleBarBorderHeight)) + measuredPlaceable += placeable + } + + return layout(constraints.maxWidth, constraints.maxHeight) { + titleBarPlaceable?.placeRelative(0, 0) + titleBarBorderPlaceable?.placeRelative(0, titleBarHeight) + + measuredPlaceable.forEach { + it.placeRelative(0, titleBarHeight + titleBarBorderHeight) + } + } + } +} + +@Immutable @JvmInline +value class DecoratedWindowState(val state: ULong) { + + @Stable val isActive: Boolean + get() = state and Active != 0UL + + @Stable val isFullscreen: Boolean + get() = state and Fullscreen != 0UL + + @Stable val isMinimized: Boolean + get() = state and Minimize != 0UL + + @Stable val isMaximized: Boolean + get() = state and Maximize != 0UL + + fun copy( + fullscreen: Boolean = isFullscreen, + minimized: Boolean = isMinimized, + maximized: Boolean = isMaximized, + active: Boolean = isActive, + ) = of( + fullscreen = fullscreen, + minimized = minimized, + maximized = maximized, + active = active, + ) + + override fun toString() = "${javaClass.simpleName}(isFullscreen=$isFullscreen, isActive=$isActive)" + + companion object { + + val Active = 1UL shl 0 + val Fullscreen = 1UL shl 1 + val Minimize = 1UL shl 2 + val Maximize = 1UL shl 3 + + fun of( + fullscreen: Boolean = false, + minimized: Boolean = false, + maximized: Boolean = false, + active: Boolean = true, + ) = DecoratedWindowState( + state = (if (fullscreen) Fullscreen else 0UL) or + (if (minimized) Minimize else 0UL) or + (if (maximized) Maximize else 0UL) or + (if (active) Active else 0UL), + ) + + fun of( + window: ComposeWindow, + ) = of( + window.placement == WindowPlacement.Fullscreen, + window.isMinimized, + window.placement == WindowPlacement.Maximized, + window.isActive, + ) + } +} + +internal data class TitleBarInfo( + val title: String, + val icon: Painter?, +) + +internal val LocalTitleBarInfo = compositionLocalOf { + error("CompositionLocal LocalTitleBarInfo not provided, TitleBar must be used in DecoratedWindow") +} diff --git a/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/Theme.kt b/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/Theme.kt new file mode 100644 index 000000000..690b7d151 --- /dev/null +++ b/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/Theme.kt @@ -0,0 +1,19 @@ +package org.jetbrains.jewel.window + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import org.jetbrains.jewel.IntelliJTheme +import org.jetbrains.jewel.window.styling.DecoratedWindowStyle +import org.jetbrains.jewel.window.styling.LocalDecoratedWindowStyle +import org.jetbrains.jewel.window.styling.LocalTitleBarStyle +import org.jetbrains.jewel.window.styling.TitleBarStyle + +val IntelliJTheme.Companion.defaultTitleBarStyle: TitleBarStyle + @Composable + @ReadOnlyComposable + get() = LocalTitleBarStyle.current + +val IntelliJTheme.Companion.defaultDecoratedWindowStyle: DecoratedWindowStyle + @Composable + @ReadOnlyComposable + get() = LocalDecoratedWindowStyle.current diff --git a/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/TitleBar.Linux.kt b/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/TitleBar.Linux.kt new file mode 100644 index 000000000..ae684bc0b --- /dev/null +++ b/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/TitleBar.Linux.kt @@ -0,0 +1,101 @@ +package org.jetbrains.jewel.window + +import androidx.compose.foundation.focusable +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.PointerButton +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.onPointerEvent +import androidx.compose.ui.platform.LocalViewConfiguration +import androidx.compose.ui.unit.dp +import com.jetbrains.JBR +import org.jetbrains.jewel.Icon +import org.jetbrains.jewel.IconButton +import org.jetbrains.jewel.IntelliJTheme +import org.jetbrains.jewel.LocalResourceLoader +import org.jetbrains.jewel.styling.IconButtonStyle +import org.jetbrains.jewel.styling.PainterProvider +import org.jetbrains.jewel.window.styling.TitleBarStyle +import java.awt.Frame +import java.awt.event.MouseEvent +import java.awt.event.WindowEvent + +@Composable internal fun DecoratedWindowScope.TitleBarOnLinux( + modifier: Modifier = Modifier, + gradientStartColor: Color = Color.Unspecified, + style: TitleBarStyle = IntelliJTheme.defaultTitleBarStyle, + content: @Composable TitleBarScope.(DecoratedWindowState) -> Unit, +) { + var lastPress = 0L + val viewConfig = LocalViewConfiguration.current + TitleBarImpl( + modifier.onPointerEvent(PointerEventType.Press, PointerEventPass.Main) { + if (this.currentEvent.button == PointerButton.Primary && this.currentEvent.changes.any { changed -> !changed.isConsumed }) { + JBR.getWindowMove()?.startMovingTogetherWithMouse(window, MouseEvent.BUTTON1) + if (System.currentTimeMillis() - lastPress in viewConfig.doubleTapMinTimeMillis..viewConfig.doubleTapTimeoutMillis) { + if (state.isMaximized) { + window.extendedState = Frame.NORMAL + } else { + window.extendedState = Frame.MAXIMIZED_BOTH + } + } + lastPress = System.currentTimeMillis() + } + }, + gradientStartColor, + style, + { size, state -> + PaddingValues(0.dp) + }, + ) { state -> + CloseButton({ + window.dispatchEvent(WindowEvent(window, WindowEvent.WINDOW_CLOSING)) + }, state, style) + + if (state.isMaximized) { + ControlButton({ + window.extendedState = Frame.NORMAL + }, state, style.icons.restoreButton, "Restore") + } else { + ControlButton({ + window.extendedState = Frame.MAXIMIZED_BOTH + }, state, style.icons.maximizeButton, "Maximize") + } + ControlButton({ + window.extendedState = Frame.ICONIFIED + }, state, style.icons.minimizeButton, "Minimize") + content(state) + } +} + +@Composable private fun TitleBarScope.CloseButton( + onClick: () -> Unit, + state: DecoratedWindowState, + style: TitleBarStyle = IntelliJTheme.defaultTitleBarStyle, +) { + ControlButton(onClick, state, style.icons.closeButton, "Close", style, style.paneCloseButtonStyle) +} + +@Composable private fun TitleBarScope.ControlButton( + onClick: () -> Unit, + state: DecoratedWindowState, + painterProvider: PainterProvider, + description: String, + style: TitleBarStyle = IntelliJTheme.defaultTitleBarStyle, + iconButtonStyle: IconButtonStyle = style.paneButtonStyle, +) { + IconButton( + onClick, + Modifier.align(Alignment.End) + .focusable(false) + .size(style.metrics.titlePaneButtonSize), + style = iconButtonStyle, + ) { + Icon(painterProvider.getPainter(LocalResourceLoader.current, state).value, description) + } +} diff --git a/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/TitleBar.MacOS.kt b/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/TitleBar.MacOS.kt new file mode 100644 index 000000000..8699defac --- /dev/null +++ b/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/TitleBar.MacOS.kt @@ -0,0 +1,105 @@ +package org.jetbrains.jewel.window + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.node.ModifierNodeElement +import androidx.compose.ui.platform.InspectorInfo +import androidx.compose.ui.platform.debugInspectorInfo +import androidx.compose.ui.unit.dp +import com.jetbrains.JBR +import org.jetbrains.jewel.IntelliJTheme +import org.jetbrains.jewel.window.styling.TitleBarStyle +import org.jetbrains.jewel.window.utils.macos.MacUtil + +fun Modifier.newFullscreenControls(newControls: Boolean = true): Modifier { + return then( + NewFullscreenControlsElement( + newControls, + inspectorInfo = debugInspectorInfo { + name = "newFullscreenControls" + value = newControls + }, + ), + ) +} + +private class NewFullscreenControlsElement( + val newControls: Boolean, + val inspectorInfo: InspectorInfo.() -> Unit, +) : ModifierNodeElement() { + + override fun create(): NewFullscreenControlsNode = NewFullscreenControlsNode(newControls) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + val otherModifier = other as? NewFullscreenControlsElement ?: return false + return newControls == otherModifier.newControls + } + + override fun hashCode(): Int = newControls.hashCode() + + override fun InspectorInfo.inspectableProperties() { + inspectorInfo() + } + + override fun update(node: NewFullscreenControlsNode) { + node.newControls = newControls + } +} + +private class NewFullscreenControlsNode( + var newControls: Boolean, +) : Modifier.Node() + +@Composable internal fun DecoratedWindowScope.TitleBarOnMacOs( + modifier: Modifier = Modifier, + gradientStartColor: Color = Color.Unspecified, + style: TitleBarStyle = IntelliJTheme.defaultTitleBarStyle, + content: @Composable TitleBarScope.(DecoratedWindowState) -> Unit, +) { + val newFullscreenControls = modifier.foldOut(false) { e, r -> + if (e is NewFullscreenControlsElement) { + e.newControls + } else { + r + } + } + + if (newFullscreenControls) { + System.setProperty("apple.awt.newFullScreeControls", true.toString()) + System.setProperty( + "apple.awt.newFullScreeControls.background", + "${style.colors.fullscreenControlButtonsBackground.toArgb()}", + ) + MacUtil.updateColors(window) + } else { + System.clearProperty("apple.awt.newFullScreeControls") + System.clearProperty("apple.awt.newFullScreeControls.background") + } + + val titleBar = remember { JBR.getWindowDecorations().createCustomTitleBar() } + + TitleBarImpl( + modifier.customTitleBarMouseEventHandler(titleBar), + gradientStartColor, + style, + { height, state -> + if (state.isFullscreen) { + MacUtil.updateFullScreenButtons(window) + } + titleBar.height = height.value + JBR.getWindowDecorations().setCustomTitleBar(window, titleBar) + + if (state.isFullscreen && newFullscreenControls) { + PaddingValues(start = 80.dp) + } else { + PaddingValues(start = titleBar.leftInset.dp, end = titleBar.rightInset.dp) + } + }, + content, + ) +} diff --git a/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/TitleBar.Windows.kt b/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/TitleBar.Windows.kt new file mode 100644 index 000000000..cb041ba02 --- /dev/null +++ b/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/TitleBar.Windows.kt @@ -0,0 +1,63 @@ +package org.jetbrains.jewel.window + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.unit.dp +import com.jetbrains.JBR +import com.jetbrains.WindowDecorations.CustomTitleBar +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.isActive +import org.jetbrains.jewel.IntelliJTheme +import org.jetbrains.jewel.util.isDark +import org.jetbrains.jewel.window.styling.TitleBarStyle + +@Composable internal fun DecoratedWindowScope.TitleBarOnWindows( + modifier: Modifier = Modifier, + gradientStartColor: Color = Color.Unspecified, + style: TitleBarStyle = IntelliJTheme.defaultTitleBarStyle, + content: @Composable TitleBarScope.(DecoratedWindowState) -> Unit, +) { + val titleBar = remember { JBR.getWindowDecorations().createCustomTitleBar() } + + TitleBarImpl( + modifier.customTitleBarMouseEventHandler(titleBar), + gradientStartColor, + style, + { height, state -> + titleBar.height = height.value + titleBar.putProperty("controls.dark", style.colors.background.isDark()) + JBR.getWindowDecorations().setCustomTitleBar(window, titleBar) + PaddingValues(start = titleBar.leftInset.dp, end = titleBar.rightInset.dp) + }, + content, + ) +} + +internal fun Modifier.customTitleBarMouseEventHandler(titleBar: CustomTitleBar): Modifier = this.pointerInput(Unit) { + val currentContext = currentCoroutineContext() + awaitPointerEventScope { + var inUserControl = false + while (currentContext.isActive) { + val event = awaitPointerEvent(PointerEventPass.Main) + event.changes.forEach { + if (!it.isConsumed && !inUserControl) { + titleBar.forceHitTest(false) + } else { + if (event.type == PointerEventType.Press) { + inUserControl = true + } + if (event.type == PointerEventType.Release) { + inUserControl = false + } + titleBar.forceHitTest(true) + } + } + } + } +} diff --git a/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/TitleBar.kt b/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/TitleBar.kt new file mode 100644 index 000000000..a3ebffa25 --- /dev/null +++ b/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/TitleBar.kt @@ -0,0 +1,297 @@ +package org.jetbrains.jewel.window + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.focusProperties +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.isUnspecified +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.Measurable +import androidx.compose.ui.layout.MeasurePolicy +import androidx.compose.ui.layout.MeasureResult +import androidx.compose.ui.layout.MeasureScope +import androidx.compose.ui.layout.Placeable +import androidx.compose.ui.layout.layoutId +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.node.ModifierNodeElement +import androidx.compose.ui.node.ParentDataModifierNode +import androidx.compose.ui.platform.InspectableValue +import androidx.compose.ui.platform.InspectorInfo +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.NoInspectorInfo +import androidx.compose.ui.platform.debugInspectorInfo +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.offset +import org.jetbrains.jewel.IntelliJTheme +import org.jetbrains.jewel.LocalContentColor +import org.jetbrains.jewel.onBackground +import org.jetbrains.jewel.styling.LocalDropdownStyle +import org.jetbrains.jewel.styling.LocalIconButtonStyle +import org.jetbrains.jewel.window.styling.TitleBarStyle +import org.jetbrains.jewel.window.utils.DesktopPlatform +import org.jetbrains.jewel.window.utils.macos.MacUtil +import java.awt.Window +import kotlin.math.max + +internal const val TITLE_BAR_COMPONENT_LAYOUT_ID_PREFIX = "__TITLE_BAR_" + +internal const val TITLE_BAR_LAYOUT_ID = "__TITLE_BAR_CONTENT__" + +internal const val TITLE_BAR_BORDER_LAYOUT_ID = "__TITLE_BAR_BORDER__" + +@Composable fun DecoratedWindowScope.TitleBar( + modifier: Modifier = Modifier, + gradientStartColor: Color = Color.Unspecified, + style: TitleBarStyle = IntelliJTheme.defaultTitleBarStyle, + content: @Composable TitleBarScope.(DecoratedWindowState) -> Unit, +) { + when (DesktopPlatform.Current) { + DesktopPlatform.Linux -> TitleBarOnLinux(modifier, gradientStartColor, style, content) + DesktopPlatform.Windows -> TitleBarOnWindows(modifier, gradientStartColor, style, content) + DesktopPlatform.MacOS -> TitleBarOnMacOs(modifier, gradientStartColor, style, content) + DesktopPlatform.Unknown -> error("TitleBar is not supported on this platform(${System.getProperty("os.name")})") + } +} + +@Composable internal fun DecoratedWindowScope.TitleBarImpl( + modifier: Modifier = Modifier, + gradientStartColor: Color = Color.Unspecified, + style: TitleBarStyle = IntelliJTheme.defaultTitleBarStyle, + applyTitleBar: (Dp, DecoratedWindowState) -> PaddingValues, + content: @Composable TitleBarScope.(DecoratedWindowState) -> Unit, +) { + val titleBarInfo = LocalTitleBarInfo.current + + val background by style.colors.backgroundFor(state) + + val density = LocalDensity.current + + val backgroundBrush = remember(background, gradientStartColor) { + if (gradientStartColor.isUnspecified) { + SolidColor(background) + } else { + with(density) { + Brush.horizontalGradient( + 0.0f to background, + 0.5f to gradientStartColor, + 1.0f to background, + startX = style.metrics.gradientStartX.toPx(), + endX = style.metrics.gradientEndX.toPx(), + ) + } + } + } + + Layout( + { + CompositionLocalProvider( + LocalContentColor provides style.colors.content, + LocalIconButtonStyle provides style.iconButtonStyle, + LocalDropdownStyle provides style.dropdownStyle, + ) { + onBackground(background) { + val scope = TitleBarScopeImpl(titleBarInfo.title, titleBarInfo.icon) + scope.content(state) + } + } + }, + modifier.background(backgroundBrush) + .focusProperties { canFocus = false } + .layoutId(TITLE_BAR_LAYOUT_ID) + .height(style.metrics.height) + .onSizeChanged { + with(density) { + applyTitleBar(it.height.toDp(), state) + } + } + .fillMaxWidth(), + measurePolicy = rememberTitleBarMeasurePolicy( + window, + state, + applyTitleBar, + ), + ) + + Spacer(Modifier.layoutId(TITLE_BAR_BORDER_LAYOUT_ID).height(1.dp).fillMaxWidth().background(style.colors.border)) +} + +internal class TitleBarMeasurePolicy( + private val window: Window, + private val state: DecoratedWindowState, + private val applyTitleBar: (Dp, DecoratedWindowState) -> PaddingValues, +) : MeasurePolicy { + + override fun MeasureScope.measure(measurables: List, constraints: Constraints): MeasureResult { + if (measurables.isEmpty()) { + return layout( + constraints.minWidth, + constraints.minHeight, + ) {} + } + + var occupiedSpaceHorizontally = 0 + + var maxSpaceVertically = constraints.minHeight + val contentConstraints = constraints.copy(minWidth = 0, minHeight = 0) + val measuredPlaceable = mutableListOf>() + + for (it in measurables) { + val placeable = it.measure(contentConstraints.offset(horizontal = -occupiedSpaceHorizontally)) + if (constraints.maxWidth < occupiedSpaceHorizontally + placeable.width) { + break + } + occupiedSpaceHorizontally += placeable.width + maxSpaceVertically = max(maxSpaceVertically, placeable.height) + measuredPlaceable += it to placeable + } + + val boxHeight = maxSpaceVertically + + val contentPadding = applyTitleBar(boxHeight.toDp(), state) + + val leftInset = contentPadding.calculateLeftPadding(layoutDirection).roundToPx() + val rightInset = contentPadding.calculateRightPadding(layoutDirection).roundToPx() + + occupiedSpaceHorizontally += leftInset + occupiedSpaceHorizontally += rightInset + + val boxWidth = maxOf(constraints.minWidth, occupiedSpaceHorizontally) + + return layout(boxWidth, boxHeight) { + if (state.isFullscreen) { + MacUtil.updateFullScreenButtons(window) + } + val placeableGroups = measuredPlaceable.groupBy { (measurable, _) -> + (measurable.parentData as? TitleBarChildDataNode)?.horizontalAlignment ?: Alignment.CenterHorizontally + } + + var headUsedSpace = leftInset + var trailerUsedSpace = rightInset + + placeableGroups[Alignment.Start]?.forEach { (measurable, placeable) -> + val x = headUsedSpace + val y = Alignment.CenterVertically.align(placeable.height, boxHeight) + placeable.placeRelative(x, y) + headUsedSpace += placeable.width + } + placeableGroups[Alignment.End]?.forEach { (measurable, placeable) -> + val x = boxWidth - placeable.width - trailerUsedSpace + val y = Alignment.CenterVertically.align(placeable.height, boxHeight) + placeable.placeRelative(x, y) + trailerUsedSpace += placeable.width + } + + val centerPlaceable = placeableGroups[Alignment.CenterHorizontally].orEmpty() + + val requiredCenterSpace = centerPlaceable.sumOf { it.second.width } + val minX = headUsedSpace + val maxX = boxWidth - trailerUsedSpace - requiredCenterSpace + var centerX = (boxWidth - requiredCenterSpace) / 2 + + if (minX <= maxX) { + if (centerX > maxX) { + centerX = maxX + } + if (centerX < minX) { + centerX = minX + } + + centerPlaceable.forEach { (measurable, placeable) -> + val x = centerX + val y = Alignment.CenterVertically.align(placeable.height, boxHeight) + placeable.placeRelative(x, y) + centerX += placeable.width + } + } + } + } +} + +@Composable internal fun rememberTitleBarMeasurePolicy( + window: Window, + state: DecoratedWindowState, + applyTitleBar: (Dp, DecoratedWindowState) -> PaddingValues, +): MeasurePolicy { + return remember(window, state, applyTitleBar) { + TitleBarMeasurePolicy( + window, + state, + applyTitleBar, + ) + } +} + +interface TitleBarScope { + + val title: String + + val icon: Painter? + + @Stable fun Modifier.align(alignment: Alignment.Horizontal): Modifier +} + +private class TitleBarScopeImpl( + override val title: String, + override val icon: Painter?, +) : TitleBarScope { + + override fun Modifier.align(alignment: Alignment.Horizontal): Modifier { + return then( + TitleBarChildDataElement( + alignment, + inspectorInfo = debugInspectorInfo { + name = "align" + value = alignment + }, + ), + ) + } +} + +private class TitleBarChildDataElement( + val horizontalAlignment: Alignment.Horizontal, + val inspectorInfo: InspectorInfo.() -> Unit = NoInspectorInfo, +) : ModifierNodeElement(), InspectableValue { + + override fun create(): TitleBarChildDataNode = TitleBarChildDataNode(horizontalAlignment) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + val otherModifier = other as? TitleBarChildDataElement ?: return false + return horizontalAlignment == otherModifier.horizontalAlignment + } + + override fun hashCode(): Int = horizontalAlignment.hashCode() + + override fun update(node: TitleBarChildDataNode) { + node.horizontalAlignment = horizontalAlignment + } + + override fun InspectorInfo.inspectableProperties() { + inspectorInfo() + } +} + +private class TitleBarChildDataNode( + var horizontalAlignment: Alignment.Horizontal, +) : ParentDataModifierNode, Modifier.Node() { + + override fun Density.modifyParentData(parentData: Any?) = this@TitleBarChildDataNode +} diff --git a/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/styling/DecoratedWindowStyling.kt b/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/styling/DecoratedWindowStyling.kt new file mode 100644 index 000000000..3506a630b --- /dev/null +++ b/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/styling/DecoratedWindowStyling.kt @@ -0,0 +1,40 @@ +package org.jetbrains.jewel.window.styling + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import org.jetbrains.jewel.window.DecoratedWindowState + +interface DecoratedWindowStyle { + + val colors: DecoratedWindowColors + val metrics: DecoratedWindowMetrics +} + +@Stable +interface DecoratedWindowColors { + + val border: Color + val borderInactive: Color + + @Composable + fun borderFor(state: DecoratedWindowState) = rememberUpdatedState( + when { + !state.isActive -> borderInactive + else -> border + }, + ) +} + +@Stable +interface DecoratedWindowMetrics { + + val borderWidth: Dp +} + +val LocalDecoratedWindowStyle = staticCompositionLocalOf { + error("No DecoratedWindowStyle provided") +} diff --git a/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/styling/TitleBarStyling.kt b/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/styling/TitleBarStyling.kt new file mode 100644 index 000000000..8f9790258 --- /dev/null +++ b/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/styling/TitleBarStyling.kt @@ -0,0 +1,91 @@ +package org.jetbrains.jewel.window.styling + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpSize +import org.jetbrains.jewel.styling.DropdownStyle +import org.jetbrains.jewel.styling.IconButtonStyle +import org.jetbrains.jewel.styling.PainterProvider +import org.jetbrains.jewel.window.DecoratedWindowState + +@Stable +interface TitleBarStyle { + + val colors: TitleBarColors + val metrics: TitleBarMetrics + val icons: TitleBarIcons + + val dropdownStyle: DropdownStyle + val iconButtonStyle: IconButtonStyle + val paneButtonStyle: IconButtonStyle + val paneCloseButtonStyle: IconButtonStyle +} + +@Stable +interface TitleBarColors { + + val background: Color + val inactiveBackground: Color + val content: Color + val border: Color + + // The background color for newControlButtons(three circles in left top corner) in MacOS fullscreen mode + val fullscreenControlButtonsBackground: Color + + // The hover and press background color for window control buttons(minimize, maximize) in Linux + val titlePaneButtonHoverBackground: Color + val titlePaneButtonPressBackground: Color + + // The hover and press background color for window close button in Linux + val titlePaneCloseButtonHoverBackground: Color + val titlePaneCloseButtonPressBackground: Color + + // The hover and press background color for IconButtons in title bar content + val iconButtonHoverBackground: Color + val iconButtonPressBackground: Color + + // The hover and press background color for Dropdown in title bar content + val dropdownPressBackground: Color + val dropdownHoverBackground: Color + + @Composable + fun backgroundFor(state: DecoratedWindowState) = rememberUpdatedState( + when { + !state.isActive -> inactiveBackground + else -> background + }, + ) +} + +@Stable +interface TitleBarMetrics { + + val height: Dp + + val gradientStartX: Dp + + val gradientEndX: Dp + + val titlePaneButtonSize: DpSize +} + +@Immutable +interface TitleBarIcons { + + val minimizeButton: PainterProvider + + val maximizeButton: PainterProvider + + val restoreButton: PainterProvider + + val closeButton: PainterProvider +} + +val LocalTitleBarStyle = staticCompositionLocalOf { + error("No TitleBarStyle provided") +} diff --git a/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/utils/DesktopPlatform.kt b/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/utils/DesktopPlatform.kt new file mode 100644 index 000000000..b42669349 --- /dev/null +++ b/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/utils/DesktopPlatform.kt @@ -0,0 +1,21 @@ +package org.jetbrains.jewel.window.utils + +enum class DesktopPlatform { + Linux, + Windows, + MacOS, + Unknown, + ; + + companion object { + val Current: DesktopPlatform by lazy { + val name = System.getProperty("os.name") + when { + name?.startsWith("Linux") == true -> Linux + name?.startsWith("Win") == true -> Windows + name == "Mac OS X" -> MacOS + else -> Unknown + } + } + } +} diff --git a/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/utils/JnaLoader.kt b/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/utils/JnaLoader.kt new file mode 100644 index 000000000..c44cc077b --- /dev/null +++ b/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/utils/JnaLoader.kt @@ -0,0 +1,43 @@ +package org.jetbrains.jewel.window.utils + +import com.sun.jna.Native +import java.util.logging.Level +import java.util.logging.Logger +import kotlin.system.measureTimeMillis + +internal object JnaLoader { + + private var loaded: Boolean? = null + private val logger = Logger.getLogger(JnaLoader::class.java.simpleName) + + @Synchronized fun load() { + if (loaded == null) { + loaded = false + try { + val time = measureTimeMillis { + Native.POINTER_SIZE + } + logger.info("JNA library (${Native.POINTER_SIZE shl 3}-bit) loaded in $time ms") + loaded = true + } catch (@Suppress("TooGenericExceptionCaught") t: Throwable) { + logger.log( + Level.WARNING, + "Unable to load JNA library(os=${ + System.getProperty("os.name") + } ${System.getProperty("os.version")}, jna.boot.library.path=${ + System.getProperty("jna.boot.library.path") + })", + t, + ) + } + } + } + + @get:Synchronized val isLoaded: Boolean + get() { + if (loaded == null) { + load() + } + return loaded ?: false + } +} diff --git a/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/utils/UnsafeAccessing.kt b/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/utils/UnsafeAccessing.kt new file mode 100644 index 000000000..2dd553873 --- /dev/null +++ b/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/utils/UnsafeAccessing.kt @@ -0,0 +1,84 @@ +package org.jetbrains.jewel.window.utils + +import sun.misc.Unsafe +import java.lang.reflect.AccessibleObject +import java.util.logging.Level +import java.util.logging.Logger + +internal object UnsafeAccessing { + + private val logger = Logger.getLogger(UnsafeAccessing::class.java.simpleName) + + private val unsafe: Any? by lazy { + try { + val theUnsafe = Unsafe::class.java.getDeclaredField("theUnsafe") + theUnsafe.isAccessible = true + theUnsafe.get(null) as Unsafe + } catch (@Suppress("TooGenericExceptionCaught") error: Throwable) { + logger.log(Level.WARNING, "Unsafe accessing initializing failed.", error) + null + } + } + + val desktopModule by lazy { + ModuleLayer.boot().findModule("java.desktop").get() + } + + val ownerModule: Module by lazy { + this.javaClass.module + } + + private val isAccessibleFieldOffset: Long? by lazy { + try { + (unsafe as? Unsafe)?.objectFieldOffset(Parent::class.java.getDeclaredField("first")) + } catch (_: Throwable) { + null + } + } + + private val implAddOpens by lazy { + try { + Module::class.java.getDeclaredMethod( + "implAddOpens", + String::class.java, + Module::class.java, + ).accessible() + } catch (_: Throwable) { + null + } + } + + fun assignAccessibility(obj: AccessibleObject) { + try { + val theUnsafe = unsafe as? Unsafe ?: return + val offset = isAccessibleFieldOffset ?: return + theUnsafe.putBooleanVolatile(obj, offset, true) + } catch (_: Throwable) { + // ignore + } + } + + fun assignAccessibility(module: Module, packages: List) { + try { + packages.forEach { + implAddOpens?.invoke(module, it, ownerModule) + } + } catch (_: Throwable) { + // ignore + } + } + + private class Parent { + + var first = false + + @Volatile + var second: Any? = null + } +} + +internal fun T.accessible(): T { + return apply { + UnsafeAccessing.assignAccessibility(this) + } +} diff --git a/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/utils/macos/Foundation.kt b/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/utils/macos/Foundation.kt new file mode 100644 index 000000000..193b29c7e --- /dev/null +++ b/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/utils/macos/Foundation.kt @@ -0,0 +1,98 @@ +package org.jetbrains.jewel.window.utils.macos + +import com.sun.jna.Function +import com.sun.jna.Library +import com.sun.jna.Native +import com.sun.jna.Pointer +import org.jetbrains.jewel.window.utils.JnaLoader +import java.lang.reflect.Proxy +import java.util.Arrays +import java.util.Collections +import java.util.logging.Level +import java.util.logging.Logger + +internal object Foundation { + + private val logger = Logger.getLogger(Foundation::class.java.simpleName) + + init { + if (!JnaLoader.isLoaded) { + logger.log(Level.WARNING, "JNA is not loaded") + } + } + + private val myFoundationLibrary: FoundationLibrary? by lazy { + try { + Native.load( + "Foundation", + FoundationLibrary::class.java, + Collections.singletonMap("jna.encoding", "UTF8"), + ) + } catch (_: Throwable) { + null + } + } + + private val myObjcMsgSend: Function? by lazy { + try { + (Proxy.getInvocationHandler(myFoundationLibrary) as Library.Handler).nativeLibrary.getFunction("objc_msgSend") + } catch (_: Throwable) { + null + } + } + + /** + * Get the ID of the NSClass with className + */ + fun getObjcClass(className: String?): ID? = myFoundationLibrary?.objc_getClass(className) + + fun getProtocol(name: String?): ID? = myFoundationLibrary?.objc_getProtocol(name) + + fun createSelector(s: String?): Pointer? = myFoundationLibrary?.sel_registerName(s) + + private fun prepInvoke(id: ID?, selector: Pointer?, args: Array): Array { + val invokArgs = arrayOfNulls(args.size + 2) + invokArgs[0] = id + invokArgs[1] = selector + System.arraycopy(args, 0, invokArgs, 2, args.size) + return invokArgs + } + + // objc_msgSend is called with the calling convention of the target method + // on x86_64 this does not make a difference, but arm64 uses a different calling convention for varargs + // it is therefore important to not call objc_msgSend as a vararg function + operator fun invoke(id: ID?, selector: Pointer?, vararg args: Any?): ID = + ID(myObjcMsgSend?.invokeLong(prepInvoke(id, selector, args)) ?: 0) + + /** + * Invokes the given vararg selector. + * Expects `NSArray arrayWithObjects:(id), ...` like signature, i.e. exactly one fixed argument, followed by varargs. + */ + fun invokeVarArg(id: ID?, selector: Pointer?, vararg args: Any?): ID { + // c functions and objc methods have at least 1 fixed argument, we therefore need to separate out the first argument + return myFoundationLibrary?.objc_msgSend( + id, + selector, + args[0], + *Arrays.copyOfRange(args, 1, args.size), + ) ?: ID.NIL + } + + operator fun invoke(cls: String?, selector: String?, vararg args: Any?): ID = + invoke(getObjcClass(cls), createSelector(selector), *args) + + fun invokeVarArg(cls: String?, selector: String?, vararg args: Any?): ID = + invokeVarArg(getObjcClass(cls), createSelector(selector), *args) + + fun safeInvoke(stringCls: String?, stringSelector: String?, vararg args: Any?): ID { + val cls = getObjcClass(stringCls) + val selector = createSelector(stringSelector) + if (!invoke(cls, "respondsToSelector:", selector).booleanValue()) { + error("Missing selector $stringSelector for $stringCls") + } + return invoke(cls, selector, *args) + } + + operator fun invoke(id: ID?, selector: String?, vararg args: Any?): ID = + invoke(id, createSelector(selector), *args) +} diff --git a/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/utils/macos/FoundationLibrary.kt b/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/utils/macos/FoundationLibrary.kt new file mode 100644 index 000000000..a4c93807e --- /dev/null +++ b/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/utils/macos/FoundationLibrary.kt @@ -0,0 +1,89 @@ +package org.jetbrains.jewel.window.utils.macos + +import com.sun.jna.Callback +import com.sun.jna.Library +import com.sun.jna.Pointer + +internal interface FoundationLibrary : Library { + fun NSLog(pString: Pointer?, thing: Any?) + fun NSFullUserName(): ID? + fun objc_allocateClassPair(supercls: ID?, name: String?, extraBytes: Int): ID? + fun objc_registerClassPair(cls: ID?) + fun CFStringCreateWithBytes( + allocator: Pointer?, + bytes: ByteArray?, + byteCount: Int, + encoding: Int, + isExternalRepresentation: Byte, + ): ID? + + fun CFStringGetCString(theString: ID?, buffer: ByteArray?, bufferSize: Int, encoding: Int): Byte + fun CFStringGetLength(theString: ID?): Int + fun CFStringConvertNSStringEncodingToEncoding(nsEncoding: Long): Long + fun CFStringConvertEncodingToIANACharSetName(cfEncoding: Long): ID? + fun CFStringConvertIANACharSetNameToEncoding(encodingName: ID?): Long + fun CFStringConvertEncodingToNSStringEncoding(cfEncoding: Long): Long + fun CFRetain(cfTypeRef: ID?) + fun CFRelease(cfTypeRef: ID?) + fun CFGetRetainCount(cfTypeRef: Pointer?): Int + fun objc_getClass(className: String?): ID? + fun objc_getProtocol(name: String?): ID? + fun class_createInstance(pClass: ID?, extraBytes: Int): ID? + fun sel_registerName(selectorName: String?): Pointer? + fun class_replaceMethod(cls: ID?, selName: Pointer?, impl: Callback?, types: String?): ID? + fun objc_getMetaClass(name: String?): ID? + + /** + * Note: Vararg version. Should only be used only for selectors with a single fixed argument followed by varargs. + */ + fun objc_msgSend(receiver: ID?, selector: Pointer?, firstArg: Any?, vararg args: Any?): ID? + fun class_respondsToSelector(cls: ID?, selName: Pointer?): Boolean + fun class_addMethod(cls: ID?, selName: Pointer?, imp: Callback?, types: String?): Boolean + fun class_addMethod(cls: ID?, selName: Pointer?, imp: ID?, types: String?): Boolean + fun class_addProtocol(aClass: ID?, protocol: ID?): Boolean + fun class_isMetaClass(cls: ID?): Boolean + fun NSStringFromSelector(selector: Pointer?): ID? + fun NSStringFromClass(aClass: ID?): ID? + fun objc_getClass(clazz: Pointer?): Pointer? + + companion object { + const val kCFStringEncodingMacRoman = 0 + const val kCFStringEncodingWindowsLatin1 = 0x0500 + const val kCFStringEncodingISOLatin1 = 0x0201 + const val kCFStringEncodingNextStepLatin = 0x0B01 + const val kCFStringEncodingASCII = 0x0600 + const val kCFStringEncodingUnicode = 0x0100 + const val kCFStringEncodingUTF8 = 0x08000100 + const val kCFStringEncodingNonLossyASCII = 0x0BFF + const val kCFStringEncodingUTF16 = 0x0100 + const val kCFStringEncodingUTF16BE = 0x10000100 + const val kCFStringEncodingUTF16LE = 0x14000100 + const val kCFStringEncodingUTF32 = 0x0c000100 + const val kCFStringEncodingUTF32BE = 0x18000100 + const val kCFStringEncodingUTF32LE = 0x1c000100 + + // https://developer.apple.com/library/mac/documentation/Carbon/Reference/CGWindow_Reference/Constants/Constants.html#//apple_ref/doc/constant_group/Window_List_Option_Constants + const val kCGWindowListOptionAll = 0 + const val kCGWindowListOptionOnScreenOnly = 1 + const val kCGWindowListOptionOnScreenAboveWindow = 2 + const val kCGWindowListOptionOnScreenBelowWindow = 4 + const val kCGWindowListOptionIncludingWindow = 8 + const val kCGWindowListExcludeDesktopElements = 16 + + // https://developer.apple.com/library/mac/documentation/Carbon/Reference/CGWindow_Reference/Constants/Constants.html#//apple_ref/doc/constant_group/Window_Image_Types + const val kCGWindowImageDefault = 0 + const val kCGWindowImageBoundsIgnoreFraming = 1 + const val kCGWindowImageShouldBeOpaque = 2 + const val kCGWindowImageOnlyShadows = 4 + const val kCGWindowImageBestResolution = 8 + const val kCGWindowImageNominalResolution = 16 + + // see enum NSBitmapImageFileType + const val NSBitmapImageFileTypeTIFF = 0 + const val NSBitmapImageFileTypeBMP = 1 + const val NSBitmapImageFileTypeGIF = 2 + const val NSBitmapImageFileTypeJPEG = 3 + const val NSBitmapImageFileTypePNG = 4 + const val NSBitmapImageFileTypeJPEG2000 = 5 + } +} diff --git a/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/utils/macos/ID.kt b/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/utils/macos/ID.kt new file mode 100644 index 000000000..b6a0864b4 --- /dev/null +++ b/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/utils/macos/ID.kt @@ -0,0 +1,26 @@ +package org.jetbrains.jewel.window.utils.macos + +import com.sun.jna.NativeLong + +/** + * Could be an address in memory (if pointer to a class or method) or a value (like 0 or 1) + */ +internal class ID : NativeLong { + constructor() + constructor(peer: Long) : super(peer) + + fun booleanValue(): Boolean = toInt() != 0 + + override fun toByte(): Byte = toInt().toByte() + + override fun toChar(): Char = toInt().toChar() + + override fun toShort(): Short = toInt().toShort() + + override fun toInt(): Int = super.toInt() + + companion object { + @JvmField + val NIL = ID(0L) + } +} diff --git a/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/utils/macos/MacUtil.kt b/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/utils/macos/MacUtil.kt new file mode 100644 index 000000000..0f5ba844c --- /dev/null +++ b/decorated-window/src/main/kotlin/org/jetbrains/jewel/window/utils/macos/MacUtil.kt @@ -0,0 +1,95 @@ +package org.jetbrains.jewel.window.utils.macos + +import org.jetbrains.jewel.window.utils.UnsafeAccessing +import org.jetbrains.jewel.window.utils.accessible +import java.awt.Component +import java.awt.Window +import java.lang.reflect.InvocationTargetException +import java.util.logging.Level +import java.util.logging.Logger +import javax.swing.SwingUtilities + +internal object MacUtil { + + private val logger = Logger.getLogger(MacUtil::class.java.simpleName) + + init { + try { + UnsafeAccessing.assignAccessibility( + UnsafeAccessing.desktopModule, + listOf("sun.awt", "sun.lwawt", "sun.lwawt.macosx"), + ) + } catch (@Suppress("TooGenericExceptionCaught") e: Exception) { + logger.log(Level.WARNING, "Assign access for jdk.desktop failed.", e) + } + } + + fun getWindowFromJavaWindow(w: Window?): ID { + if (w == null) { + return ID.NIL + } + try { + val cPlatformWindow = getPlatformWindow(w) + if (cPlatformWindow != null) { + val ptr = cPlatformWindow.javaClass.superclass.getDeclaredField("ptr") + ptr.setAccessible(true) + return ID(ptr.getLong(cPlatformWindow)) + } + } catch (e: IllegalAccessException) { + logger.log(Level.WARNING, "Fail to get cPlatformWindow from awt window.", e) + } catch (e: NoSuchFieldException) { + logger.log(Level.WARNING, "Fail to get cPlatformWindow from awt window.", e) + } + return ID.NIL + } + + fun getPlatformWindow(w: Window): Any? { + try { + val awtAccessor = Class.forName("sun.awt.AWTAccessor") + val componentAccessor = awtAccessor.getMethod("getComponentAccessor").invoke(null) + val getPeer = componentAccessor.javaClass.getMethod("getPeer", Component::class.java).accessible() + val peer = getPeer.invoke(componentAccessor, w) + if (peer != null) { + val cWindowPeerClass: Class<*> = peer.javaClass + val getPlatformWindowMethod = cWindowPeerClass.getDeclaredMethod("getPlatformWindow") + val cPlatformWindow = getPlatformWindowMethod.invoke(peer) + if (cPlatformWindow != null) { + return cPlatformWindow + } + } + } catch (e: NoSuchMethodException) { + logger.log(Level.WARNING, "Fail to get cPlatformWindow from awt window.", e) + } catch (e: IllegalAccessException) { + logger.log(Level.WARNING, "Fail to get cPlatformWindow from awt window.", e) + } catch (e: InvocationTargetException) { + logger.log(Level.WARNING, "Fail to get cPlatformWindow from awt window.", e) + } catch (e: ClassNotFoundException) { + logger.log(Level.WARNING, "Fail to get cPlatformWindow from awt window.", e) + } + return null + } + + fun updateColors(w: Window) { + SwingUtilities.invokeLater { + val window = MacUtil.getWindowFromJavaWindow(w) + val delegate = Foundation.invoke(window, "delegate") + if (Foundation.invoke(delegate, "respondsToSelector:", Foundation.createSelector("updateColors")) + .booleanValue() + ) { + Foundation.invoke(delegate, "updateColors") + } + } + } + + fun updateFullScreenButtons(w: Window) { + SwingUtilities.invokeLater { + val selector = Foundation.createSelector("updateFullScreenButtons") + val window = getWindowFromJavaWindow(w) + val delegate = Foundation.invoke(window, "delegate") + + if (Foundation.invoke(delegate, "respondsToSelector:", selector).booleanValue()) { + Foundation.invoke(delegate, "updateFullScreenButtons") + } + } + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 370a2df4d..b88ef3b08 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,6 +14,7 @@ kotlinxSerialization = "1.5.1" kotlinpoet = "1.14.2" semVer = "1.2.0" simpleXml = "2.7.1" +jna = "5.13.0" [libraries] javaSarif = { module = "com.contrastsecurity:java-sarif", version.ref = "javaSarif" } @@ -31,6 +32,8 @@ ij-platform-core-ui-233 = { module = "com.jetbrains.intellij.platform:core-ui", semVer = { module = "net.swiftzer.semver:semver", version.ref = "semVer" } simpleXml = { module = "org.simpleframework:simple-xml", version.ref = "simpleXml" } +jna-core = { module = "net.java.dev.jna:jna", version.ref = "jna" } + # Plugin libraries for build-logic's convention plugins to use to resolve the types/tasks coming from these plugins detekt-gradlePlugin = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" } kotlin-gradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } diff --git a/int-ui/int-ui-core/src/main/kotlin/org/jetbrains/jewel/intui/core/IntUiThemeDefinition.kt b/int-ui/int-ui-core/src/main/kotlin/org/jetbrains/jewel/intui/core/IntUiThemeDefinition.kt index dfbe84266..420289191 100644 --- a/int-ui/int-ui-core/src/main/kotlin/org/jetbrains/jewel/intui/core/IntUiThemeDefinition.kt +++ b/int-ui/int-ui-core/src/main/kotlin/org/jetbrains/jewel/intui/core/IntUiThemeDefinition.kt @@ -1,6 +1,7 @@ package org.jetbrains.jewel.intui.core import androidx.compose.runtime.Immutable +import androidx.compose.runtime.ProvidedValue import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle import org.jetbrains.jewel.GlobalColors @@ -17,8 +18,32 @@ class IntUiThemeDefinition( override val globalMetrics: GlobalMetrics, override val defaultTextStyle: TextStyle, override val contentColor: Color, + override val extensionStyles: Array> = emptyArray(), ) : IntelliJThemeDefinition { + override fun withExtensions(vararg extensions: ProvidedValue<*>): IntUiThemeDefinition = + copy(extensionStyles = extensionStyles + extensions) + + fun copy( + isDark: Boolean = this.isDark, + globalColors: GlobalColors = this.globalColors, + colorPalette: IntUiThemeColorPalette = this.colorPalette, + iconData: IntelliJThemeIconData = this.iconData, + globalMetrics: GlobalMetrics = this.globalMetrics, + defaultTextStyle: TextStyle = this.defaultTextStyle, + contentColor: Color = this.contentColor, + extensionStyles: Array> = this.extensionStyles, + ): IntUiThemeDefinition = IntUiThemeDefinition( + isDark = isDark, + globalColors = globalColors, + colorPalette = colorPalette, + iconData = iconData, + globalMetrics = globalMetrics, + defaultTextStyle = defaultTextStyle, + contentColor = contentColor, + extensionStyles = extensionStyles, + ) + override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false @@ -31,6 +56,8 @@ class IntUiThemeDefinition( if (iconData != other.iconData) return false if (globalMetrics != other.globalMetrics) return false if (defaultTextStyle != other.defaultTextStyle) return false + if (contentColor != other.contentColor) return false + if (!extensionStyles.contentEquals(other.extensionStyles)) return false return true } @@ -42,10 +69,8 @@ class IntUiThemeDefinition( result = 31 * result + iconData.hashCode() result = 31 * result + globalMetrics.hashCode() result = 31 * result + defaultTextStyle.hashCode() + result = 31 * result + contentColor.hashCode() + result = 31 * result + extensionStyles.contentHashCode() return result } - - override fun toString(): String = - "IntUiThemeDefinition(isDark=$isDark, globalColors=$globalColors, colorPalette=$colorPalette, " + - "iconData=$iconData, metrics=$globalMetrics, defaultTextStyle=$defaultTextStyle)" } diff --git a/int-ui/int-ui-decorated-window/build.gradle.kts b/int-ui/int-ui-decorated-window/build.gradle.kts new file mode 100644 index 000000000..3f6e04f83 --- /dev/null +++ b/int-ui/int-ui-decorated-window/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + jewel + `jewel-publish` + alias(libs.plugins.composeDesktop) +} + +dependencies { + api(projects.decoratedWindow) + api(projects.intUi.intUiStandalone) +} diff --git a/int-ui/int-ui-decorated-window/src/main/kotlin/org/jetbrains/jewel/intui/window/IntUiTheme.kt b/int-ui/int-ui-decorated-window/src/main/kotlin/org/jetbrains/jewel/intui/window/IntUiTheme.kt new file mode 100644 index 000000000..e9f753358 --- /dev/null +++ b/int-ui/int-ui-decorated-window/src/main/kotlin/org/jetbrains/jewel/intui/window/IntUiTheme.kt @@ -0,0 +1,32 @@ +package org.jetbrains.jewel.intui.window + +import androidx.compose.runtime.Composable +import org.jetbrains.jewel.intui.core.IntUiThemeDefinition +import org.jetbrains.jewel.intui.window.styling.IntUiDecoratedWindowStyle +import org.jetbrains.jewel.intui.window.styling.IntUiTitleBarStyle +import org.jetbrains.jewel.window.styling.DecoratedWindowStyle +import org.jetbrains.jewel.window.styling.LocalDecoratedWindowStyle +import org.jetbrains.jewel.window.styling.LocalTitleBarStyle +import org.jetbrains.jewel.window.styling.TitleBarStyle + +@Composable +fun IntUiThemeDefinition.decoratedWindowStyle(): DecoratedWindowStyle = + if (isDark) { + IntUiDecoratedWindowStyle.dark() + } else { + IntUiDecoratedWindowStyle.light() + } + +@Composable +fun IntUiThemeDefinition.withDecoratedWindow( + titleBarStyle: TitleBarStyle = if (isDark) { + IntUiTitleBarStyle.dark() + } else { + IntUiTitleBarStyle.light() + }, +): IntUiThemeDefinition { + return withExtensions( + LocalDecoratedWindowStyle provides decoratedWindowStyle(), + LocalTitleBarStyle provides titleBarStyle, + ) +} diff --git a/int-ui/int-ui-decorated-window/src/main/kotlin/org/jetbrains/jewel/intui/window/styling/IntUiDecoratedWindowStyling.kt b/int-ui/int-ui-decorated-window/src/main/kotlin/org/jetbrains/jewel/intui/window/styling/IntUiDecoratedWindowStyling.kt new file mode 100644 index 000000000..f14e20df8 --- /dev/null +++ b/int-ui/int-ui-decorated-window/src/main/kotlin/org/jetbrains/jewel/intui/window/styling/IntUiDecoratedWindowStyling.kt @@ -0,0 +1,118 @@ +package org.jetbrains.jewel.intui.window.styling + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import org.jetbrains.jewel.window.styling.DecoratedWindowColors +import org.jetbrains.jewel.window.styling.DecoratedWindowMetrics +import org.jetbrains.jewel.window.styling.DecoratedWindowStyle + +@Stable +@Immutable +class IntUiDecoratedWindowStyle( + override val colors: IntUiDecoratedWindowColors, + override val metrics: IntUiDecoratedWindowMetrics, +) : DecoratedWindowStyle { + + override fun hashCode(): Int { + var result = colors.hashCode() + result = 31 * result + metrics.hashCode() + return result + } + + override fun toString(): String = "IntUiDecoratedWindowStyle(colors=$colors, metrics=$metrics)" + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is IntUiDecoratedWindowStyle) return false + + if (colors != other.colors) return false + if (metrics != other.metrics) return false + + return true + } + + companion object { + + @Composable fun light( + colors: IntUiDecoratedWindowColors = IntUiDecoratedWindowColors.light(), + metrics: IntUiDecoratedWindowMetrics = IntUiDecoratedWindowMetrics(), + ): IntUiDecoratedWindowStyle = IntUiDecoratedWindowStyle(colors, metrics) + + @Composable fun dark( + colors: IntUiDecoratedWindowColors = IntUiDecoratedWindowColors.dark(), + metrics: IntUiDecoratedWindowMetrics = IntUiDecoratedWindowMetrics(), + ): IntUiDecoratedWindowStyle = IntUiDecoratedWindowStyle(colors, metrics) + } +} + +@Stable +@Immutable +class IntUiDecoratedWindowColors( + override val border: Color, + override val borderInactive: Color, +) : DecoratedWindowColors { + + override fun hashCode(): Int { + var result = border.hashCode() + result = 31 * result + borderInactive.hashCode() + return result + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is IntUiDecoratedWindowColors) return false + + if (border != other.border) return false + if (borderInactive != other.borderInactive) return false + + return true + } + + override fun toString(): String = "IntUiDecoratedWindowColors(border=$border, borderInactive=$borderInactive)" + + companion object { + + @Composable + fun light( + // from Window.undecorated.border + borderColor: Color = Color(0xFF5A5D6B), + inactiveBorderColor: Color = borderColor, + ) = IntUiDecoratedWindowColors( + borderColor, + inactiveBorderColor, + ) + + @Composable + fun dark( + // from Window.undecorated.border + borderColor: Color = Color(0xFF5A5D63), + inactiveBorderColor: Color = borderColor, + ) = IntUiDecoratedWindowColors( + borderColor, + inactiveBorderColor, + ) + } +} + +@Stable +class IntUiDecoratedWindowMetrics( + override val borderWidth: Dp = 1.dp, +) : DecoratedWindowMetrics { + + override fun toString(): String = "IntUiDecoratedWindowMetrics(borderWidth=$borderWidth)" + + override fun hashCode(): Int = borderWidth.hashCode() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is IntUiDecoratedWindowMetrics) return false + + if (borderWidth != other.borderWidth) return false + + return true + } +} diff --git a/int-ui/int-ui-decorated-window/src/main/kotlin/org/jetbrains/jewel/intui/window/styling/IntUiTitleBarStyling.kt b/int-ui/int-ui-decorated-window/src/main/kotlin/org/jetbrains/jewel/intui/window/styling/IntUiTitleBarStyling.kt new file mode 100644 index 000000000..013d3179b --- /dev/null +++ b/int-ui/int-ui-decorated-window/src/main/kotlin/org/jetbrains/jewel/intui/window/styling/IntUiTitleBarStyling.kt @@ -0,0 +1,566 @@ +package org.jetbrains.jewel.intui.window.styling + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.shape.CornerSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import org.jetbrains.jewel.IntelliJIconMapper +import org.jetbrains.jewel.InternalJewelApi +import org.jetbrains.jewel.LocalIconData +import org.jetbrains.jewel.SvgLoader +import org.jetbrains.jewel.intui.core.theme.IntUiDarkTheme +import org.jetbrains.jewel.intui.core.theme.IntUiLightTheme +import org.jetbrains.jewel.intui.standalone.rememberSvgLoader +import org.jetbrains.jewel.intui.standalone.styling.IntUiDropdownColors +import org.jetbrains.jewel.intui.standalone.styling.IntUiDropdownMetrics +import org.jetbrains.jewel.intui.standalone.styling.IntUiDropdownStyle +import org.jetbrains.jewel.intui.standalone.styling.IntUiIconButtonColors +import org.jetbrains.jewel.intui.standalone.styling.IntUiIconButtonMetrics +import org.jetbrains.jewel.intui.standalone.styling.IntUiIconButtonStyle +import org.jetbrains.jewel.intui.standalone.styling.IntUiMenuStyle +import org.jetbrains.jewel.styling.DropdownStyle +import org.jetbrains.jewel.styling.IconButtonStyle +import org.jetbrains.jewel.styling.MenuStyle +import org.jetbrains.jewel.styling.PainterProvider +import org.jetbrains.jewel.styling.ResourcePainterProvider +import org.jetbrains.jewel.styling.SimpleResourcePathPatcher +import org.jetbrains.jewel.window.DecoratedWindowState +import org.jetbrains.jewel.window.styling.TitleBarColors +import org.jetbrains.jewel.window.styling.TitleBarIcons +import org.jetbrains.jewel.window.styling.TitleBarMetrics +import org.jetbrains.jewel.window.styling.TitleBarStyle + +@Stable +@Immutable +class IntUiTitleBarStyle( + override val colors: IntUiTitleBarColors, + override val metrics: IntUiTitleBarMetrics, + override val icons: TitleBarIcons, + override val dropdownStyle: DropdownStyle, + override val iconButtonStyle: IconButtonStyle, + override val paneButtonStyle: IconButtonStyle, + override val paneCloseButtonStyle: IconButtonStyle, +) : TitleBarStyle { + + override fun hashCode(): Int { + var result = colors.hashCode() + result = 31 * result + metrics.hashCode() + result = 31 * result + icons.hashCode() + result = 31 * result + dropdownStyle.hashCode() + result = 31 * result + iconButtonStyle.hashCode() + result = 31 * result + paneButtonStyle.hashCode() + result = 31 * result + paneCloseButtonStyle.hashCode() + return result + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is IntUiTitleBarStyle) return false + + if (colors != other.colors) return false + if (metrics != other.metrics) return false + if (icons != other.icons) return false + if (dropdownStyle != other.dropdownStyle) return false + if (iconButtonStyle != other.iconButtonStyle) return false + + return true + } + + override fun toString(): String = "IntUiTitleBarStyle(colors=$colors, " + + "metrics=$metrics, " + + "icons=$icons, " + + "dropdownStyle=$dropdownStyle, " + + "iconButtonStyle=$iconButtonStyle, " + + "paneButtonStyle=$paneButtonStyle, " + + "paneCloseButtonStyle=$paneCloseButtonStyle)" + + companion object { + + @Composable + private fun titleBarDropdownStyle( + svgLoader: SvgLoader, + content: Color, + hoverBackground: Color, + pressBackground: Color, + menuStyle: MenuStyle, + ) = IntUiDropdownStyle.undecorated( + svgLoader, + IntUiDropdownColors.undecorated( + backgroundHovered = hoverBackground, + backgroundPressed = pressBackground, + content = content, + iconTint = Color.Unspecified, + ), + metrics = IntUiDropdownMetrics( + arrowMinSize = DpSize(30.dp, 30.dp), + cornerSize = CornerSize(6.dp), + minSize = DpSize((23 + 6).dp, 30.dp), + contentPadding = PaddingValues(top = 3.dp, bottom = 3.dp, start = 6.dp, end = 0.dp), + borderWidth = 0.dp, + ), + menuStyle = menuStyle, + ) + + @Composable + private fun titleBarIconButtonStyle( + hoverBackground: Color, + pressBackground: Color, + metrics: IntUiIconButtonMetrics, + ) = IntUiIconButtonStyle( + IntUiIconButtonColors( + background = Color.Transparent, + backgroundDisabled = Color.Transparent, + backgroundFocused = Color.Transparent, + backgroundPressed = hoverBackground, + backgroundHovered = pressBackground, + border = Color.Transparent, + borderDisabled = Color.Transparent, + borderFocused = Color.Transparent, + borderPressed = Color.Transparent, + borderHovered = Color.Transparent, + ), + metrics, + ) + + @Composable + fun light( + svgLoader: SvgLoader = rememberSvgLoader(false).value, + colors: IntUiTitleBarColors = IntUiTitleBarColors.light(), + metrics: IntUiTitleBarMetrics = IntUiTitleBarMetrics(), + icons: IntUiTitleBarIcons = intUiTitleBarIcons(svgLoader), + ): IntUiTitleBarStyle = IntUiTitleBarStyle( + colors = colors, + metrics = metrics, + icons = icons, + dropdownStyle = titleBarDropdownStyle( + svgLoader, + colors.content, + colors.dropdownHoverBackground, + colors.dropdownPressBackground, + IntUiMenuStyle.light(svgLoader), + ), + iconButtonStyle = titleBarIconButtonStyle( + colors.iconButtonHoverBackground, + colors.iconButtonPressBackground, + IntUiIconButtonMetrics(borderWidth = 0.dp), + ), + paneButtonStyle = titleBarIconButtonStyle( + colors.titlePaneButtonHoverBackground, + colors.titlePaneButtonPressBackground, + IntUiIconButtonMetrics(CornerSize(0.dp), borderWidth = 0.dp), + ), + paneCloseButtonStyle = titleBarIconButtonStyle( + colors.titlePaneCloseButtonHoverBackground, + colors.titlePaneCloseButtonPressBackground, + IntUiIconButtonMetrics(CornerSize(0.dp), borderWidth = 0.dp), + ), + ) + + @Composable + fun lightWithLightHeader( + svgLoader: SvgLoader = rememberSvgLoader(false).value, + colors: IntUiTitleBarColors = IntUiTitleBarColors.lightWithLightHeader(), + metrics: IntUiTitleBarMetrics = IntUiTitleBarMetrics(), + icons: IntUiTitleBarIcons = intUiTitleBarIcons(svgLoader), + ): IntUiTitleBarStyle = IntUiTitleBarStyle( + colors = colors, + metrics = metrics, + icons = icons, + dropdownStyle = titleBarDropdownStyle( + svgLoader, + colors.content, + colors.dropdownHoverBackground, + colors.dropdownPressBackground, + IntUiMenuStyle.light(svgLoader), + ), + iconButtonStyle = titleBarIconButtonStyle( + colors.iconButtonHoverBackground, + colors.iconButtonPressBackground, + IntUiIconButtonMetrics(borderWidth = 0.dp), + ), + paneButtonStyle = titleBarIconButtonStyle( + colors.titlePaneButtonHoverBackground, + colors.titlePaneButtonPressBackground, + IntUiIconButtonMetrics(CornerSize(0.dp), borderWidth = 0.dp), + ), + paneCloseButtonStyle = titleBarIconButtonStyle( + colors.titlePaneCloseButtonHoverBackground, + colors.titlePaneCloseButtonPressBackground, + IntUiIconButtonMetrics(CornerSize(0.dp), borderWidth = 0.dp), + ), + ) + + @Composable + fun dark( + svgLoader: SvgLoader = rememberSvgLoader(true).value, + colors: IntUiTitleBarColors = IntUiTitleBarColors.dark(), + metrics: IntUiTitleBarMetrics = IntUiTitleBarMetrics(), + icons: IntUiTitleBarIcons = intUiTitleBarIcons(svgLoader), + ): IntUiTitleBarStyle = IntUiTitleBarStyle( + colors = colors, + metrics = metrics, + icons = icons, + dropdownStyle = titleBarDropdownStyle( + svgLoader, + colors.content, + colors.dropdownHoverBackground, + colors.dropdownPressBackground, + IntUiMenuStyle.dark(svgLoader), + ), + iconButtonStyle = titleBarIconButtonStyle( + colors.iconButtonHoverBackground, + colors.iconButtonPressBackground, + IntUiIconButtonMetrics(borderWidth = 0.dp), + ), + paneButtonStyle = titleBarIconButtonStyle( + colors.titlePaneButtonHoverBackground, + colors.titlePaneButtonPressBackground, + IntUiIconButtonMetrics(CornerSize(0.dp), borderWidth = 0.dp), + ), + paneCloseButtonStyle = titleBarIconButtonStyle( + colors.titlePaneCloseButtonHoverBackground, + colors.titlePaneCloseButtonPressBackground, + IntUiIconButtonMetrics(CornerSize(0.dp), borderWidth = 0.dp), + ), + ) + } +} + +@Stable +@Immutable +class IntUiTitleBarColors( + override val background: Color, + override val inactiveBackground: Color, + override val content: Color, + override val border: Color, + override val fullscreenControlButtonsBackground: Color, + override val titlePaneButtonHoverBackground: Color, + override val titlePaneButtonPressBackground: Color, + override val titlePaneCloseButtonHoverBackground: Color, + override val titlePaneCloseButtonPressBackground: Color, + override val iconButtonHoverBackground: Color, + override val iconButtonPressBackground: Color, + override val dropdownHoverBackground: Color, + override val dropdownPressBackground: Color, +) : TitleBarColors { + + override fun hashCode(): Int { + var result = background.hashCode() + result = 31 * result + inactiveBackground.hashCode() + result = 31 * result + content.hashCode() + result = 31 * result + border.hashCode() + result = 31 * result + fullscreenControlButtonsBackground.hashCode() + result = 31 * result + titlePaneButtonHoverBackground.hashCode() + result = 31 * result + titlePaneButtonPressBackground.hashCode() + result = 31 * result + titlePaneCloseButtonHoverBackground.hashCode() + result = 31 * result + titlePaneCloseButtonPressBackground.hashCode() + result = 31 * result + iconButtonHoverBackground.hashCode() + result = 31 * result + iconButtonPressBackground.hashCode() + result = 31 * result + dropdownHoverBackground.hashCode() + result = 31 * result + dropdownPressBackground.hashCode() + return result + } + + override fun toString(): String { + return "IntUiTitleBarColors(background=$background, " + + "inactiveBackground=$inactiveBackground, " + + "content=$content, " + + "border=$border, " + + "fullscreenControlButtonsBackground=$fullscreenControlButtonsBackground, " + + "titlePaneButtonHoverBackground=$titlePaneButtonHoverBackground, " + + "titlePaneButtonPressBackground=$titlePaneButtonPressBackground, " + + "titlePaneCloseButtonHoverBackground=$titlePaneCloseButtonHoverBackground, " + + "titlePaneCloseButtonPressBackground=$titlePaneCloseButtonPressBackground, " + + "iconButtonHoverBackground=$iconButtonHoverBackground, " + + "iconButtonPressBackground=$iconButtonPressBackground, " + + "dropdownHoverBackground=$dropdownHoverBackground, " + + "dropdownPressBackground=$dropdownPressBackground)" + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is IntUiTitleBarColors) return false + + if (background != other.background) return false + if (inactiveBackground != other.inactiveBackground) return false + if (content != other.content) return false + if (border != other.border) return false + if (fullscreenControlButtonsBackground != other.fullscreenControlButtonsBackground) return false + if (titlePaneButtonHoverBackground != other.titlePaneButtonHoverBackground) return false + if (titlePaneButtonPressBackground != other.titlePaneButtonPressBackground) return false + if (titlePaneCloseButtonHoverBackground != other.titlePaneCloseButtonHoverBackground) return false + if (titlePaneCloseButtonPressBackground != other.titlePaneCloseButtonPressBackground) return false + if (iconButtonHoverBackground != other.iconButtonHoverBackground) return false + if (iconButtonPressBackground != other.iconButtonPressBackground) return false + if (dropdownHoverBackground != other.dropdownHoverBackground) return false + if (dropdownPressBackground != other.dropdownPressBackground) return false + + return true + } + + companion object { + + @Composable + fun light( + backgroundColor: Color = IntUiLightTheme.colors.grey(2), + inactiveBackground: Color = IntUiLightTheme.colors.grey(3), + contentColor: Color = IntUiLightTheme.colors.grey(12), + borderColor: Color = IntUiLightTheme.colors.grey(4), + fullscreenControlButtonsBackground: Color = Color(0xFF7A7B80), + // Color hex from + // com.intellij.util.ui.JBUI.CurrentTheme.CustomFrameDecorations.titlePaneButtonHoverBackground + titlePaneButtonHoverBackground: Color = Color(0x1AFFFFFF), + // Same as + // com.intellij.util.ui.JBUI.CurrentTheme.CustomFrameDecorations.titlePaneButtonPressBackground + titlePaneButtonPressBackground: Color = titlePaneButtonHoverBackground, + // Color hex from + // com.intellij.openapi.wm.impl.customFrameDecorations.CustomFrameTitleButtons.closeStyleBuilder + titlePaneCloseButtonHoverBackground: Color = Color(0xFFE81123), + titlePaneCloseButtonPressBackground: Color = Color(0xFFF1707A), + + iconButtonHoverBackground: Color = IntUiLightTheme.colors.grey(3), + iconButtonPressBackground: Color = IntUiLightTheme.colors.grey(3), + + // There are two fields in theme.json: transparentHoverBackground and hoverBackground, + // but in com.intellij.ide.ui.laf.darcula.ui.ToolbarComboWidgetUI#paintBackground, + // transparentHoverBackground is used first, which is guessed to be due to the gradient background + // caused by the project color of the titlebar, which makes the pure color background look strange + // in the area. In order to simplify the use in Jewel, here directly use transparentHoverBackground + // as hoverBackground. + dropdownHoverBackground: Color = Color(0x1AFFFFFF), + dropdownPressBackground: Color = dropdownHoverBackground, + ) = IntUiTitleBarColors( + background = backgroundColor, + inactiveBackground = inactiveBackground, + content = contentColor, + border = borderColor, + fullscreenControlButtonsBackground = fullscreenControlButtonsBackground, + titlePaneButtonHoverBackground = titlePaneButtonHoverBackground, + titlePaneButtonPressBackground = titlePaneButtonPressBackground, + titlePaneCloseButtonHoverBackground = titlePaneCloseButtonHoverBackground, + titlePaneCloseButtonPressBackground = titlePaneCloseButtonPressBackground, + iconButtonHoverBackground = iconButtonHoverBackground, + iconButtonPressBackground = iconButtonPressBackground, + dropdownHoverBackground = dropdownHoverBackground, + dropdownPressBackground = dropdownPressBackground, + ) + + @Composable + fun lightWithLightHeader( + backgroundColor: Color = IntUiLightTheme.colors.grey(13), + inactiveBackground: Color = IntUiLightTheme.colors.grey(12), + fullscreenControlButtonsBackground: Color = Color(0xFF7A7B80), + contentColor: Color = IntUiLightTheme.colors.grey(1), + borderColor: Color = IntUiLightTheme.colors.grey(11), + titlePaneButtonHoverBackground: Color = Color(0x1A000000), + titlePaneButtonPressBackground: Color = titlePaneButtonHoverBackground, + titlePaneCloseButtonHoverBackground: Color = Color(0xFFE81123), + titlePaneCloseButtonPressBackground: Color = Color(0xFFF1707A), + iconButtonHoverBackground: Color = IntUiLightTheme.colors.grey(12), + iconButtonPressBackground: Color = IntUiLightTheme.colors.grey(11), + dropdownHoverBackground: Color = Color(0x0D000000), + dropdownPressBackground: Color = dropdownHoverBackground, + ) = IntUiTitleBarColors( + background = backgroundColor, + inactiveBackground = inactiveBackground, + content = contentColor, + border = borderColor, + fullscreenControlButtonsBackground = fullscreenControlButtonsBackground, + titlePaneButtonHoverBackground = titlePaneButtonHoverBackground, + titlePaneButtonPressBackground = titlePaneButtonPressBackground, + titlePaneCloseButtonHoverBackground = titlePaneCloseButtonHoverBackground, + titlePaneCloseButtonPressBackground = titlePaneCloseButtonPressBackground, + iconButtonHoverBackground = iconButtonHoverBackground, + iconButtonPressBackground = iconButtonPressBackground, + dropdownHoverBackground = dropdownHoverBackground, + dropdownPressBackground = dropdownPressBackground, + ) + + @Composable + fun dark( + backgroundColor: Color = IntUiDarkTheme.colors.grey(2), + inactiveBackground: Color = IntUiDarkTheme.colors.grey(3), + fullscreenControlButtonsBackground: Color = Color(0xFF575A5C), + contentColor: Color = IntUiDarkTheme.colors.grey(12), + borderColor: Color = IntUiDarkTheme.colors.grey(4), + titlePaneButtonHoverBackground: Color = Color(0x1AFFFFFF), + titlePaneButtonPressBackground: Color = titlePaneButtonHoverBackground, + titlePaneCloseButtonHoverBackground: Color = Color(0xFFE81123), + titlePaneCloseButtonPressBackground: Color = Color(0xFFF1707A), + iconButtonHoverBackground: Color = IntUiLightTheme.colors.grey(3), + iconButtonPressBackground: Color = IntUiLightTheme.colors.grey(3), + dropdownHoverBackground: Color = Color(0x1AFFFFFF), + dropdownPressBackground: Color = dropdownHoverBackground, + ) = IntUiTitleBarColors( + background = backgroundColor, + inactiveBackground = inactiveBackground, + content = contentColor, + border = borderColor, + fullscreenControlButtonsBackground = fullscreenControlButtonsBackground, + titlePaneButtonHoverBackground = titlePaneButtonHoverBackground, + titlePaneButtonPressBackground = titlePaneButtonPressBackground, + titlePaneCloseButtonHoverBackground = titlePaneCloseButtonHoverBackground, + titlePaneCloseButtonPressBackground = titlePaneCloseButtonPressBackground, + iconButtonHoverBackground = iconButtonHoverBackground, + iconButtonPressBackground = iconButtonPressBackground, + dropdownHoverBackground = dropdownHoverBackground, + dropdownPressBackground = dropdownPressBackground, + ) + } +} + +@Stable +@Immutable +class IntUiTitleBarMetrics( + override val height: Dp = 40.dp, + override val gradientStartX: Dp = (-100).dp, + override val gradientEndX: Dp = 400.dp, + override val titlePaneButtonSize: DpSize = DpSize(40.dp, 40.dp), +) : TitleBarMetrics { + + override fun hashCode(): Int { + var result = height.hashCode() + result = 31 * result + gradientStartX.hashCode() + result = 31 * result + gradientEndX.hashCode() + result = 31 * result + titlePaneButtonSize.hashCode() + return result + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is IntUiTitleBarMetrics) return false + + if (height != other.height) return false + if (gradientStartX != other.gradientStartX) return false + if (gradientEndX != other.gradientEndX) return false + if (titlePaneButtonSize != other.titlePaneButtonSize) return false + + return true + } + + override fun toString(): String = "IntUiTitleBarMetrics(height=$height, " + + "gradientStartX=$gradientStartX, " + + "gradientEndX=$gradientEndX, " + + "titlePaneButtonSize=$titlePaneButtonSize)" +} + +class IntUiTitleBarIcons( + override val minimizeButton: PainterProvider, + override val maximizeButton: PainterProvider, + override val restoreButton: PainterProvider, + override val closeButton: PainterProvider, +) : TitleBarIcons { + + override fun hashCode(): Int { + var result = minimizeButton.hashCode() + result = 31 * result + maximizeButton.hashCode() + result = 31 * result + restoreButton.hashCode() + result = 31 * result + closeButton.hashCode() + return result + } + + override fun toString(): String = "IntUiTitleBarIcons(minimizeButton=$minimizeButton, " + + "maximizeButton=$maximizeButton, " + + "restoreButton=$restoreButton, " + + "closeButton=$closeButton)" + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is IntUiTitleBarIcons) return false + + if (minimizeButton != other.minimizeButton) return false + if (maximizeButton != other.maximizeButton) return false + if (restoreButton != other.restoreButton) return false + if (closeButton != other.closeButton) return false + + return true + } + + @OptIn(InternalJewelApi::class) + companion object { + + @Composable + fun minimize( + svgLoader: SvgLoader, + basePath: String = "icons/intui/window/minimize.svg", + ): PainterProvider = ResourcePainterProvider( + basePath, + svgLoader, + IntelliJIconMapper, + LocalIconData.current, + TitleBarResourcePathPatcher(), + ) + + @Composable + fun maximize( + svgLoader: SvgLoader, + basePath: String = "icons/intui/window/maximize.svg", + ): PainterProvider = + ResourcePainterProvider( + basePath, + svgLoader, + IntelliJIconMapper, + LocalIconData.current, + TitleBarResourcePathPatcher(), + ) + + @Composable + fun restore( + svgLoader: SvgLoader, + basePath: String = "icons/intui/window/restore.svg", + ): PainterProvider = + ResourcePainterProvider( + basePath, + svgLoader, + IntelliJIconMapper, + LocalIconData.current, + TitleBarResourcePathPatcher(), + ) + + @Composable + fun close( + svgLoader: SvgLoader, + basePath: String = "icons/intui/window/close.svg", + ): PainterProvider = + ResourcePainterProvider( + basePath, + svgLoader, + IntelliJIconMapper, + LocalIconData.current, + TitleBarResourcePathPatcher(), + ) + } +} + +@Composable +fun intUiTitleBarIcons( + svgLoader: SvgLoader, + minimize: PainterProvider = IntUiTitleBarIcons.minimize(svgLoader), + maximize: PainterProvider = IntUiTitleBarIcons.maximize(svgLoader), + restore: PainterProvider = IntUiTitleBarIcons.restore(svgLoader), + close: PainterProvider = IntUiTitleBarIcons.close(svgLoader), +) = IntUiTitleBarIcons(minimize, maximize, restore, close) + +private class TitleBarResourcePathPatcher( + private val prefixTokensProvider: (state: DecoratedWindowState) -> String = { "" }, + private val suffixTokensProvider: (state: DecoratedWindowState) -> String = { "" }, +) : SimpleResourcePathPatcher() { + + @Composable + override fun injectVariantTokens(extraData: DecoratedWindowState?): String = buildString { + if (extraData == null) return@buildString + + append(prefixTokensProvider(extraData)) + + if (!extraData.isActive) { + append("Inactive") + } + + append(suffixTokensProvider(extraData)) + } +} diff --git a/int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/close.svg b/int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/close.svg new file mode 100644 index 000000000..2adb4e8b3 --- /dev/null +++ b/int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/close.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/closeActive.svg b/int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/closeActive.svg new file mode 100644 index 000000000..6c7926cc0 --- /dev/null +++ b/int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/closeActive.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/closeInactive.svg b/int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/closeInactive.svg new file mode 100644 index 000000000..3bc3d9e28 --- /dev/null +++ b/int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/closeInactive.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/closeInactive_dark.svg b/int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/closeInactive_dark.svg new file mode 100644 index 000000000..b0b0e2735 --- /dev/null +++ b/int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/closeInactive_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/close_dark.svg b/int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/close_dark.svg new file mode 100644 index 000000000..9169e8463 --- /dev/null +++ b/int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/close_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/maximize.svg b/int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/maximize.svg new file mode 100644 index 000000000..4a83737f3 --- /dev/null +++ b/int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/maximize.svg @@ -0,0 +1,4 @@ + + + + diff --git a/int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/maximizeInactive.svg b/int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/maximizeInactive.svg new file mode 100644 index 000000000..745855833 --- /dev/null +++ b/int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/maximizeInactive.svg @@ -0,0 +1,4 @@ + + + + diff --git a/int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/maximizeInactive_dark.svg b/int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/maximizeInactive_dark.svg new file mode 100644 index 000000000..b26d5f884 --- /dev/null +++ b/int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/maximizeInactive_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/maximize_dark.svg b/int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/maximize_dark.svg new file mode 100644 index 000000000..e23d4d81e --- /dev/null +++ b/int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/maximize_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/minimize.svg b/int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/minimize.svg new file mode 100644 index 000000000..5af691fd2 --- /dev/null +++ b/int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/minimize.svg @@ -0,0 +1,4 @@ + + + + diff --git a/int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/minimizeInactive.svg b/int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/minimizeInactive.svg new file mode 100644 index 000000000..0b17cb03a --- /dev/null +++ b/int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/minimizeInactive.svg @@ -0,0 +1,4 @@ + + + + diff --git a/int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/minimizeInactive_dark.svg b/int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/minimizeInactive_dark.svg new file mode 100644 index 000000000..b5a7f4395 --- /dev/null +++ b/int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/minimizeInactive_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/minimize_dark.svg b/int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/minimize_dark.svg new file mode 100644 index 000000000..ff69b584a --- /dev/null +++ b/int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/minimize_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/restore.svg b/int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/restore.svg new file mode 100644 index 000000000..e0cda0a73 --- /dev/null +++ b/int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/restore.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/restoreInactive.svg b/int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/restoreInactive.svg new file mode 100644 index 000000000..ba2ec938f --- /dev/null +++ b/int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/restoreInactive.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/restoreInactive_dark.svg b/int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/restoreInactive_dark.svg new file mode 100644 index 000000000..991ae13ec --- /dev/null +++ b/int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/restoreInactive_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/restore_dark.svg b/int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/restore_dark.svg new file mode 100644 index 000000000..9216cfaf0 --- /dev/null +++ b/int-ui/int-ui-decorated-window/src/main/resources/icons/intui/window/restore_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiDropdownStyling.kt b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiDropdownStyling.kt index 1ce35fb14..b4e82bb24 100644 --- a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiDropdownStyling.kt +++ b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiDropdownStyling.kt @@ -35,6 +35,16 @@ data class IntUiDropdownStyle( companion object { + @Composable + fun undecorated( + svgLoader: SvgLoader, + colors: IntUiDropdownColors, + metrics: IntUiDropdownMetrics = IntUiDropdownMetrics(borderWidth = 0.dp), + icons: IntUiDropdownIcons = intUiDropdownIcons(svgLoader), + textStyle: TextStyle = IntUiTheme.defaultTextStyle, + menuStyle: MenuStyle = IntUiMenuStyle.light(svgLoader), + ) = IntUiDropdownStyle(colors, metrics, icons, textStyle, menuStyle) + @Composable fun light( svgLoader: SvgLoader, @@ -83,6 +93,37 @@ data class IntUiDropdownColors( companion object { + @Composable + fun undecorated( + backgroundPressed: Color, + backgroundHovered: Color = backgroundPressed, + content: Color, + contentDisabled: Color = content, + iconTint: Color, + iconTintDisabled: Color = iconTint, + ) = IntUiDropdownColors( + background = Color.Transparent, + backgroundDisabled = Color.Transparent, + backgroundFocused = Color.Transparent, + backgroundPressed = backgroundPressed, + backgroundHovered = backgroundHovered, + content = content, + contentDisabled = contentDisabled, + contentFocused = content, + contentPressed = content, + contentHovered = content, + border = Color.Transparent, + borderDisabled = Color.Transparent, + borderFocused = Color.Transparent, + borderPressed = Color.Transparent, + borderHovered = Color.Transparent, + iconTint = iconTint, + iconTintDisabled = iconTintDisabled, + iconTintFocused = iconTint, + iconTintPressed = iconTint, + iconTintHovered = iconTint, + ) + @Composable fun light( background: Color = IntUiLightTheme.colors.grey(14), @@ -141,7 +182,7 @@ data class IntUiDropdownColors( contentPressed: Color = content, contentHovered: Color = content, border: Color = IntUiDarkTheme.colors.grey(5), - borderDisabled: Color = IntUiDarkTheme.colors.grey(11), + borderDisabled: Color = IntUiDarkTheme.colors.grey(5), borderFocused: Color = IntUiDarkTheme.colors.blue(6), borderPressed: Color = border, borderHovered: Color = border, diff --git a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiMenuStyling.kt b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiMenuStyling.kt index 670784e6c..16cbc77c3 100644 --- a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiMenuStyling.kt +++ b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiMenuStyling.kt @@ -182,7 +182,7 @@ data class IntUiMenuMetrics( override val cornerSize: CornerSize = CornerSize(8.dp), override val menuMargin: PaddingValues = PaddingValues(vertical = 6.dp), override val contentPadding: PaddingValues = PaddingValues(vertical = 8.dp), - override val offset: DpOffset = DpOffset(0.dp, 2.dp), + override val offset: DpOffset = DpOffset((-6).dp, 2.dp), override val shadowSize: Dp = 12.dp, override val borderWidth: Dp = 1.dp, override val itemMetrics: MenuItemMetrics = IntUiMenuItemMetrics(), diff --git a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiTooltipStyling.kt b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiTooltipStyling.kt index 91fa66728..6ffec8520 100644 --- a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiTooltipStyling.kt +++ b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiTooltipStyling.kt @@ -4,8 +4,10 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.shape.CornerSize import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable +import androidx.compose.ui.Alignment import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import org.jetbrains.jewel.intui.core.theme.IntUiDarkTheme import org.jetbrains.jewel.intui.core.theme.IntUiLightTheme @@ -78,4 +80,6 @@ data class IntUiTooltipMetrics( override val cornerSize: CornerSize = CornerSize(5.dp), override val borderWidth: Dp = 1.dp, override val shadowSize: Dp = 12.dp, + override val tooltipOffset: DpOffset = DpOffset(0.dp, 20.dp), + override val tooltipAlignment: Alignment.Horizontal = Alignment.Start, ) : TooltipMetrics diff --git a/samples/standalone/build.gradle.kts b/samples/standalone/build.gradle.kts index f7651827e..e2fa0ace1 100644 --- a/samples/standalone/build.gradle.kts +++ b/samples/standalone/build.gradle.kts @@ -9,6 +9,7 @@ plugins { dependencies { implementation(projects.intUi.intUiStandalone) + implementation(projects.intUi.intUiDecoratedWindow) implementation(compose.desktop.currentOs) { exclude(group = "org.jetbrains.compose.material") } diff --git a/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/IntUiThemes.kt b/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/IntUiThemes.kt new file mode 100644 index 000000000..651f0393a --- /dev/null +++ b/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/IntUiThemes.kt @@ -0,0 +1,9 @@ +package org.jetbrains.jewel.samples.standalone + +enum class IntUiThemes { + Light, LightWithLightHeader, Dark; + + fun isDark() = this == Dark + + fun isLightHeader() = this == LightWithLightHeader +} diff --git a/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/Main.kt b/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/Main.kt index 1ed2fd48b..4f3990c6e 100644 --- a/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/Main.kt +++ b/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/Main.kt @@ -9,7 +9,9 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollbarAdapter @@ -18,23 +20,32 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState 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.graphics.painter.Painter import androidx.compose.ui.res.ResourceLoader import androidx.compose.ui.res.loadSvgPainter import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.singleWindowApplication +import androidx.compose.ui.window.application import org.jetbrains.jewel.CheckboxRow import org.jetbrains.jewel.Divider +import org.jetbrains.jewel.Dropdown +import org.jetbrains.jewel.Icon +import org.jetbrains.jewel.IconButton import org.jetbrains.jewel.JewelSvgLoader import org.jetbrains.jewel.LocalResourceLoader import org.jetbrains.jewel.Orientation +import org.jetbrains.jewel.Text +import org.jetbrains.jewel.Tooltip import org.jetbrains.jewel.VerticalScrollbar import org.jetbrains.jewel.intui.standalone.IntUiTheme import org.jetbrains.jewel.intui.standalone.rememberSvgLoader +import org.jetbrains.jewel.intui.window.styling.IntUiTitleBarStyle +import org.jetbrains.jewel.intui.window.withDecoratedWindow import org.jetbrains.jewel.samples.standalone.components.Borders import org.jetbrains.jewel.samples.standalone.components.Buttons import org.jetbrains.jewel.samples.standalone.components.Checkboxes @@ -48,41 +59,132 @@ import org.jetbrains.jewel.samples.standalone.components.Tabs import org.jetbrains.jewel.samples.standalone.components.TextAreas import org.jetbrains.jewel.samples.standalone.components.TextFields import org.jetbrains.jewel.samples.standalone.components.Tooltips +import org.jetbrains.jewel.separator +import org.jetbrains.jewel.styling.rememberStatelessPainterProvider +import org.jetbrains.jewel.window.DecoratedWindow +import org.jetbrains.jewel.window.TitleBar +import org.jetbrains.jewel.window.newFullscreenControls +import java.awt.Desktop import java.io.InputStream +import java.net.URI fun main() { val icon = svgResource("icons/jewel-logo.svg") - singleWindowApplication( - title = "Jewel component catalog", - icon = icon, - ) { - var isDark by remember { mutableStateOf(false) } + application { + var intUiTheme by remember { mutableStateOf(IntUiThemes.Light) } + var swingCompat by remember { mutableStateOf(false) } - val theme = if (isDark) IntUiTheme.darkThemeDefinition() else IntUiTheme.lightThemeDefinition() + val theme = if (intUiTheme.isDark()) IntUiTheme.darkThemeDefinition() else IntUiTheme.lightThemeDefinition() + val projectColor by rememberUpdatedState( + if (intUiTheme.isLightHeader()) { + Color(0xFFF5D4C1) + } else { + Color(0xFF654B40) + }, + ) - IntUiTheme(theme, swingCompat) { + IntUiTheme( + theme.withDecoratedWindow( + titleBarStyle = when (intUiTheme) { + IntUiThemes.Light -> IntUiTitleBarStyle.light() + IntUiThemes.LightWithLightHeader -> IntUiTitleBarStyle.lightWithLightHeader() + IntUiThemes.Dark -> IntUiTitleBarStyle.dark() + }, + ), + swingCompat, + ) { val resourceLoader = LocalResourceLoader.current val svgLoader by rememberSvgLoader() - val windowBackground = if (isDark) { - IntUiTheme.colorPalette.grey(1) - } else { - IntUiTheme.colorPalette.grey(14) - } + DecoratedWindow( + onCloseRequest = { exitApplication() }, + title = "Jewel component catalog", + icon = icon, + ) { + val windowBackground = if (intUiTheme.isDark()) { + IntUiTheme.colorPalette.grey(1) + } else { + IntUiTheme.colorPalette.grey(14) + } + TitleBar(Modifier.newFullscreenControls(), gradientStartColor = projectColor) { + val jewelLogoProvider = rememberStatelessPainterProvider("icons/jewel-logo.svg", svgLoader) + val jewelLogo by jewelLogoProvider.getPainter(resourceLoader) - Column(Modifier.fillMaxSize().background(windowBackground)) { - Row( - modifier = Modifier.fillMaxWidth().padding(8.dp), - horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterHorizontally), - verticalAlignment = Alignment.CenterVertically, - ) { - CheckboxRow("Dark", isDark, resourceLoader, { isDark = it }) - CheckboxRow("Swing compat", swingCompat, resourceLoader, { swingCompat = it }) + Row(Modifier.align(Alignment.Start)) { + Dropdown(resourceLoader, Modifier.height(30.dp), menuContent = { + selectableItem(false, { + }) { + Text("New Project...") + } + separator() + selectableItem(false, { + }) { + Text("jewel") + } + }) { + Row( + horizontalArrangement = Arrangement.spacedBy(3.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon(jewelLogo, "Jewel Logo", Modifier.padding(horizontal = 4.dp).size(20.dp)) + Text("jewel") + } + } + } + + Text(title) + + Row(Modifier.align(Alignment.End)) { + Tooltip({ + Text("Open Jewel Github repository") + }) { + IconButton({ + Desktop.getDesktop().browse(URI.create("https://github.com/JetBrains/jewel")) + }, Modifier.size(40.dp).padding(5.dp)) { + val iconProvider = rememberStatelessPainterProvider("icons/github@20x20.svg", svgLoader) + Icon(iconProvider.getPainter(resourceLoader).value, "Github") + } + } + + Tooltip({ + when (intUiTheme) { + IntUiThemes.Light -> Text("Switch to light theme with light header") + IntUiThemes.LightWithLightHeader -> Text("Switch to dark theme") + IntUiThemes.Dark -> Text("Switch to light theme") + } + }) { + IconButton({ + intUiTheme = when (intUiTheme) { + IntUiThemes.Light -> IntUiThemes.LightWithLightHeader + IntUiThemes.LightWithLightHeader -> IntUiThemes.Dark + IntUiThemes.Dark -> IntUiThemes.Light + } + }, Modifier.size(40.dp).padding(5.dp)) { + val lightThemeIcon = + rememberStatelessPainterProvider("icons/lightTheme@20x20.svg", svgLoader) + val darkThemeIcon = + rememberStatelessPainterProvider("icons/darkTheme@20x20.svg", svgLoader) + + val iconProvider = if (intUiTheme.isDark()) darkThemeIcon else lightThemeIcon + Icon(iconProvider.getPainter(resourceLoader).value, "Themes") + } + } + } } - Divider(Orientation.Horizontal, Modifier.fillMaxWidth()) + Column(Modifier.fillMaxSize().background(windowBackground)) { + Row( + modifier = Modifier.fillMaxWidth().padding(8.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterHorizontally), + verticalAlignment = Alignment.CenterVertically, + ) { + CheckboxRow("Swing compat", swingCompat, resourceLoader, { swingCompat = it }) + } + + Divider(Orientation.Horizontal, Modifier.fillMaxWidth()) - ComponentShowcase(svgLoader, resourceLoader) + ComponentShowcase(svgLoader, resourceLoader) + } } } } diff --git a/samples/standalone/src/main/resources/icons/cwmAccess.svg b/samples/standalone/src/main/resources/icons/cwmAccess.svg new file mode 100644 index 000000000..5bf782005 --- /dev/null +++ b/samples/standalone/src/main/resources/icons/cwmAccess.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/samples/standalone/src/main/resources/icons/cwmAccess@20x20.svg b/samples/standalone/src/main/resources/icons/cwmAccess@20x20.svg new file mode 100644 index 000000000..4d7dfd098 --- /dev/null +++ b/samples/standalone/src/main/resources/icons/cwmAccess@20x20.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/samples/standalone/src/main/resources/icons/cwmAccess@20x20_dark.svg b/samples/standalone/src/main/resources/icons/cwmAccess@20x20_dark.svg new file mode 100644 index 000000000..aaf65f88e --- /dev/null +++ b/samples/standalone/src/main/resources/icons/cwmAccess@20x20_dark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/samples/standalone/src/main/resources/icons/cwmAccess_dark.svg b/samples/standalone/src/main/resources/icons/cwmAccess_dark.svg new file mode 100644 index 000000000..ad42caecc --- /dev/null +++ b/samples/standalone/src/main/resources/icons/cwmAccess_dark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/samples/standalone/src/main/resources/icons/darkTheme.svg b/samples/standalone/src/main/resources/icons/darkTheme.svg new file mode 100644 index 000000000..5ada6d350 --- /dev/null +++ b/samples/standalone/src/main/resources/icons/darkTheme.svg @@ -0,0 +1,3 @@ + + + diff --git a/samples/standalone/src/main/resources/icons/darkTheme@20x20.svg b/samples/standalone/src/main/resources/icons/darkTheme@20x20.svg new file mode 100644 index 000000000..5804d0e7f --- /dev/null +++ b/samples/standalone/src/main/resources/icons/darkTheme@20x20.svg @@ -0,0 +1,3 @@ + + + diff --git a/samples/standalone/src/main/resources/icons/darkTheme@20x20_dark.svg b/samples/standalone/src/main/resources/icons/darkTheme@20x20_dark.svg new file mode 100644 index 000000000..512a748f0 --- /dev/null +++ b/samples/standalone/src/main/resources/icons/darkTheme@20x20_dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/samples/standalone/src/main/resources/icons/darkTheme_dark.svg b/samples/standalone/src/main/resources/icons/darkTheme_dark.svg new file mode 100644 index 000000000..4d65a04e2 --- /dev/null +++ b/samples/standalone/src/main/resources/icons/darkTheme_dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/samples/standalone/src/main/resources/icons/github.svg b/samples/standalone/src/main/resources/icons/github.svg new file mode 100644 index 000000000..0c7982bf9 --- /dev/null +++ b/samples/standalone/src/main/resources/icons/github.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/samples/standalone/src/main/resources/icons/github@20x20.svg b/samples/standalone/src/main/resources/icons/github@20x20.svg new file mode 100644 index 000000000..bf2e15399 --- /dev/null +++ b/samples/standalone/src/main/resources/icons/github@20x20.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/samples/standalone/src/main/resources/icons/github@20x20_dark.svg b/samples/standalone/src/main/resources/icons/github@20x20_dark.svg new file mode 100644 index 000000000..f4a865d25 --- /dev/null +++ b/samples/standalone/src/main/resources/icons/github@20x20_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/samples/standalone/src/main/resources/icons/github_dark.svg b/samples/standalone/src/main/resources/icons/github_dark.svg new file mode 100644 index 000000000..d86c44553 --- /dev/null +++ b/samples/standalone/src/main/resources/icons/github_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/samples/standalone/src/main/resources/icons/lightTheme.svg b/samples/standalone/src/main/resources/icons/lightTheme.svg new file mode 100644 index 000000000..7c43d8600 --- /dev/null +++ b/samples/standalone/src/main/resources/icons/lightTheme.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/samples/standalone/src/main/resources/icons/lightTheme@20x20.svg b/samples/standalone/src/main/resources/icons/lightTheme@20x20.svg new file mode 100644 index 000000000..0ce5e3e2a --- /dev/null +++ b/samples/standalone/src/main/resources/icons/lightTheme@20x20.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/samples/standalone/src/main/resources/icons/lightTheme@20x20_dark.svg b/samples/standalone/src/main/resources/icons/lightTheme@20x20_dark.svg new file mode 100644 index 000000000..52e715437 --- /dev/null +++ b/samples/standalone/src/main/resources/icons/lightTheme@20x20_dark.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/samples/standalone/src/main/resources/icons/lightTheme_dark.svg b/samples/standalone/src/main/resources/icons/lightTheme_dark.svg new file mode 100644 index 000000000..9f0654954 --- /dev/null +++ b/samples/standalone/src/main/resources/icons/lightTheme_dark.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/samples/standalone/src/main/resources/icons/search.svg b/samples/standalone/src/main/resources/icons/search.svg index 03cb025f3..bdcd000f0 100644 --- a/samples/standalone/src/main/resources/icons/search.svg +++ b/samples/standalone/src/main/resources/icons/search.svg @@ -1,4 +1,5 @@ - - - + + + + diff --git a/samples/standalone/src/main/resources/icons/search@20x20.svg b/samples/standalone/src/main/resources/icons/search@20x20.svg new file mode 100644 index 000000000..183a6100c --- /dev/null +++ b/samples/standalone/src/main/resources/icons/search@20x20.svg @@ -0,0 +1,4 @@ + + + + diff --git a/samples/standalone/src/main/resources/icons/search@20x20_dark.svg b/samples/standalone/src/main/resources/icons/search@20x20_dark.svg new file mode 100644 index 000000000..27f0332d1 --- /dev/null +++ b/samples/standalone/src/main/resources/icons/search@20x20_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/samples/standalone/src/main/resources/icons/search_dark.svg b/samples/standalone/src/main/resources/icons/search_dark.svg new file mode 100644 index 000000000..533a8d80b --- /dev/null +++ b/samples/standalone/src/main/resources/icons/search_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/samples/standalone/src/main/resources/icons/settings.svg b/samples/standalone/src/main/resources/icons/settings.svg new file mode 100644 index 000000000..c30de8209 --- /dev/null +++ b/samples/standalone/src/main/resources/icons/settings.svg @@ -0,0 +1,4 @@ + + + + diff --git a/samples/standalone/src/main/resources/icons/settings@20x20.svg b/samples/standalone/src/main/resources/icons/settings@20x20.svg new file mode 100644 index 000000000..fbe2af4df --- /dev/null +++ b/samples/standalone/src/main/resources/icons/settings@20x20.svg @@ -0,0 +1,4 @@ + + + + diff --git a/samples/standalone/src/main/resources/icons/settings@20x20_dark.svg b/samples/standalone/src/main/resources/icons/settings@20x20_dark.svg new file mode 100644 index 000000000..cc6378305 --- /dev/null +++ b/samples/standalone/src/main/resources/icons/settings@20x20_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/samples/standalone/src/main/resources/icons/settings_dark.svg b/samples/standalone/src/main/resources/icons/settings_dark.svg new file mode 100644 index 000000000..62f328167 --- /dev/null +++ b/samples/standalone/src/main/resources/icons/settings_dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/settings.gradle.kts b/settings.gradle.kts index 0d8e601ff..9941b2705 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -27,11 +27,13 @@ dependencyResolutionManagement { include( ":core", + ":decorated-window", ":ide-laf-bridge", ":ide-laf-bridge:ide-laf-bridge-232", ":ide-laf-bridge:ide-laf-bridge-233", ":samples:standalone", ":samples:ide-plugin", ":int-ui:int-ui-core", + ":int-ui:int-ui-decorated-window", ":int-ui:int-ui-standalone", )