Skip to content

Commit

Permalink
Support window border on Linux
Browse files Browse the repository at this point in the history
  • Loading branch information
devkanro committed Oct 12, 2023
1 parent f85cc20 commit c323889
Show file tree
Hide file tree
Showing 15 changed files with 322 additions and 164 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,5 @@ interface IntelliJThemeDefinition {

val extensionStyles: Array<ProvidedValue<*>>

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

var focusManager: FocusManager? by mutableStateOf(null)
var inputModeManager: InputModeManager? by mutableStateOf(null)
var focusManager: FocusManager? by remember { mutableStateOf(null) }
var inputModeManager: InputModeManager? by remember { mutableStateOf(null) }
val menuManager = remember(onDismissRequest) {
MenuManager(onDismissRequest = onDismissRequest)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
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
Expand All @@ -18,9 +27,16 @@ 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 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.WindowAdapter
import java.awt.event.WindowEvent
import javax.swing.JFrame

@Composable fun DecoratedWindow(
Expand All @@ -35,6 +51,7 @@ import javax.swing.JFrame
alwaysOnTop: Boolean = false,
onPreviewKeyEvent: (KeyEvent) -> Boolean = { false },
onKeyEvent: (KeyEvent) -> Boolean = { false },
style: DecoratedWindowStyle = IntelliJTheme.defaultDecoratedWindowStyle,
content: @Composable DecoratedWindowScope.() -> Unit,
) {
// Using undecorated window for linux
Expand All @@ -55,23 +72,71 @@ import javax.swing.JFrame
onPreviewKeyEvent,
onKeyEvent,
) {
var decoratedWindowState by remember { mutableStateOf(DecoratedWindowState.of(window)) }

DisposableEffect(window) {
val adapter = object : WindowAdapter() {
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)
}
}
window.addWindowListener(adapter)
window.addWindowStateListener(adapter)
onDispose {
window.removeWindowListener(adapter)
window.removeWindowStateListener(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(
LocalWindow provides window,
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()
}, measurePolicy = DecoratedWindowMeasurePolicy)
}, modifier = undecoratedWindowBorder, measurePolicy = DecoratedWindowMeasurePolicy)
}
}
}

@Stable interface DecoratedWindowScope : FrameWindowScope {

override val window: ComposeWindow

val state: DecoratedWindowState
}

private object DecoratedWindowMeasurePolicy : MeasurePolicy {
Expand Down Expand Up @@ -118,6 +183,65 @@ private object DecoratedWindowMeasurePolicy : MeasurePolicy {
}
}

@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?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,17 @@ 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
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import java.awt.event.WindowEvent
modifier: Modifier = Modifier,
gradientStartColor: Color = Color.Unspecified,
style: TitleBarStyle = IntelliJTheme.defaultTitleBarStyle,
content: @Composable TitleBarScope.(TitleBarState) -> Unit,
content: @Composable TitleBarScope.(DecoratedWindowState) -> Unit,
) {
TitleBarImpl(
modifier.onPointerEvent(PointerEventType.Press, PointerEventPass.Main) {
Expand Down Expand Up @@ -67,16 +67,16 @@ import java.awt.event.WindowEvent

@Composable private fun TitleBarScope.CloseButton(
onClick: () -> Unit,
state: TitleBarState,
state: DecoratedWindowState,
style: TitleBarStyle = IntelliJTheme.defaultTitleBarStyle,
) {
ControlButton(onClick, state, style.icons.closeButton, "Close", style, style.paneCloseButtonStyle())
}

@Composable private fun TitleBarScope.ControlButton(
onClick: () -> Unit,
state: TitleBarState,
painterProvider: PainterProvider<TitleBarState>,
state: DecoratedWindowState,
painterProvider: PainterProvider<DecoratedWindowState>,
description: String,
style: TitleBarStyle = IntelliJTheme.defaultTitleBarStyle,
iconButtonStyle: IconButtonStyle = style.paneButtonStyle(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ private class NewFullscreenControlsNode(
modifier: Modifier = Modifier,
gradientStartColor: Color = Color.Unspecified,
style: TitleBarStyle = IntelliJTheme.defaultTitleBarStyle,
content: @Composable TitleBarScope.(TitleBarState) -> Unit,
content: @Composable TitleBarScope.(DecoratedWindowState) -> Unit,
) {
val newFullscreenControls = modifier.foldOut(false) { e, r ->
if (e is NewFullscreenControlsElement) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import org.jetbrains.jewel.window.styling.TitleBarStyle
modifier: Modifier = Modifier,
gradientStartColor: Color = Color.Unspecified,
style: TitleBarStyle = IntelliJTheme.defaultTitleBarStyle,
content: @Composable TitleBarScope.(TitleBarState) -> Unit,
content: @Composable TitleBarScope.(DecoratedWindowState) -> Unit,
) {
val titleBar = remember { JBR.getWindowDecorations().createCustomTitleBar() }

Expand Down
Loading

0 comments on commit c323889

Please sign in to comment.