Skip to content

Commit 322db0f

Browse files
authored
Merge pull request #6 from kdroidFilter/feat/inspector-component
feat(inspector): add Inspector component and refactor scaffold panel
2 parents 7c0f6bc + cc12195 commit 322db0f

30 files changed

Lines changed: 481 additions & 214 deletions
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package io.github.kdroidfilter.nucleus.ui.apple.macos.components
2+
3+
import androidx.compose.foundation.background
4+
import androidx.compose.foundation.layout.Box
5+
import androidx.compose.foundation.layout.Column
6+
import androidx.compose.foundation.layout.Spacer
7+
import androidx.compose.foundation.layout.fillMaxSize
8+
import androidx.compose.foundation.layout.height
9+
import androidx.compose.runtime.Composable
10+
import androidx.compose.ui.Modifier
11+
import androidx.compose.ui.graphics.Color
12+
import io.github.kdroidfilter.nucleus.ui.apple.macos.theme.GlassType
13+
import io.github.kdroidfilter.nucleus.ui.apple.macos.theme.LocalGlassType
14+
import io.github.kdroidfilter.nucleus.ui.apple.macos.theme.LocalTitleBarHeight
15+
import io.github.kdroidfilter.nucleus.ui.apple.macos.theme.LocalWindowActive
16+
import io.github.kdroidfilter.nucleus.ui.apple.macos.theme.MacosTheme
17+
18+
/**
19+
* macOS-style inspector panel for the right side of a [Scaffold].
20+
*
21+
* Handles its own background (window-active aware + tinted glass overlay),
22+
* left separator line (drawn on top, immune to glass refraction), and
23+
* title bar spacing — mirroring the [Sidebar] pattern.
24+
*
25+
* @param modifier Modifier applied to the root container.
26+
* @param content The inspector content.
27+
*/
28+
@Composable
29+
fun Inspector(
30+
modifier: Modifier = Modifier,
31+
content: @Composable () -> Unit,
32+
) {
33+
val colors = MacosTheme.colorScheme
34+
val isDark = colors.isDark
35+
val isWindowActive = LocalWindowActive.current
36+
val glassType = LocalGlassType.current
37+
val titleBarHeight = LocalTitleBarHeight.current
38+
39+
// Opaque background when the window is inactive (matches Sidebar pattern)
40+
val inactiveOverlay = if (isWindowActive) {
41+
Color.Transparent
42+
} else {
43+
if (isDark) Color(0xFF282828) else Color(0xFFF4F4F4)
44+
}
45+
46+
// Tinted glass overlay
47+
val tintedOverlay = if (glassType == GlassType.Tinted && isWindowActive) {
48+
if (isDark) Color.Black.copy(alpha = 0.25f) else Color.Black.copy(alpha = 0.08f)
49+
} else {
50+
Color.Transparent
51+
}
52+
53+
Box(
54+
modifier = modifier
55+
.fillMaxSize()
56+
.background(if (isWindowActive) colors.background else inactiveOverlay)
57+
.background(tintedOverlay),
58+
) {
59+
Column(modifier = Modifier.fillMaxSize()) {
60+
// Spacer to push content below the title bar
61+
Spacer(Modifier.height(titleBarHeight))
62+
Box(modifier = Modifier.weight(1f)) {
63+
content()
64+
}
65+
}
66+
}
67+
}

macosui/src/commonMain/kotlin/io/github/kdroidfilter/nucleus/ui/apple/macos/components/Scaffold.kt

Lines changed: 113 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ import androidx.compose.animation.expandHorizontally
77
import androidx.compose.animation.fadeIn
88
import androidx.compose.animation.fadeOut
99
import androidx.compose.animation.shrinkHorizontally
10-
import androidx.compose.animation.slideInHorizontally
11-
import androidx.compose.animation.slideOutHorizontally
1210
import androidx.compose.animation.core.animateDpAsState
1311
import androidx.compose.foundation.background
1412
import androidx.compose.foundation.layout.Box
@@ -22,6 +20,7 @@ import androidx.compose.foundation.layout.height
2220
import androidx.compose.foundation.layout.padding
2321
import androidx.compose.foundation.layout.width
2422
import androidx.compose.ui.draw.clipToBounds
23+
import androidx.compose.ui.draw.drawWithContent
2524
import androidx.compose.runtime.Composable
2625
import androidx.compose.runtime.CompositionLocalProvider
2726
import androidx.compose.runtime.getValue
@@ -40,7 +39,6 @@ import io.github.fletchmckee.liquid.liquefiable
4039
import io.github.fletchmckee.liquid.liquid
4140
import io.github.fletchmckee.liquid.rememberLiquidState
4241
import io.github.kdroidfilter.nucleus.ui.apple.macos.theme.liquidGlassFade
43-
import io.github.kdroidfilter.nucleus.ui.apple.macos.theme.MacosDuration
4442
import io.github.kdroidfilter.nucleus.ui.apple.macos.theme.SpringPreset
4543
import io.github.kdroidfilter.nucleus.ui.apple.macos.theme.MacosTheme
4644
import io.github.kdroidfilter.nucleus.ui.apple.macos.theme.LocalLiquidState
@@ -52,13 +50,17 @@ import io.github.kdroidfilter.nucleus.ui.apple.macos.theme.LocalToolbarGlassStat
5250
import io.github.kdroidfilter.nucleus.ui.apple.macos.theme.LocalSidebarHide
5351
import io.github.kdroidfilter.nucleus.ui.apple.macos.theme.LocalSidebarVisible
5452
import io.github.kdroidfilter.nucleus.ui.apple.macos.theme.macosSpring
55-
import io.github.kdroidfilter.nucleus.ui.apple.macos.theme.macosTween
5653
import androidx.compose.animation.core.FastOutSlowInEasing
5754
import androidx.compose.animation.core.tween
55+
import androidx.compose.animation.slideInHorizontally
56+
import androidx.compose.animation.slideOutHorizontally
5857
import androidx.compose.foundation.layout.offset
5958
import androidx.compose.runtime.LaunchedEffect
59+
import androidx.compose.foundation.clickable
60+
import androidx.compose.foundation.interaction.MutableInteractionSource
6061
import androidx.compose.ui.unit.IntOffset
6162
import androidx.compose.ui.unit.IntSize
63+
import androidx.compose.ui.zIndex
6264

6365
/**
6466
* macOS-style scaffold layout with optional sidebar, content list, inspector,
@@ -89,6 +91,8 @@ import androidx.compose.ui.unit.IntSize
8991
* @param titleBarHeight Height of the title bar for content inset.
9092
* @param bottomBar Optional bottom bar composable overlaying the content area.
9193
* @param bottomBarHeight Height of the bottom bar.
94+
* @param dismissPanelsOnContentTap When true (and [pushContent] is enabled), tapping the content
95+
* area dismisses any open sidebar or inspector panel.
9296
* @param content Main content area. Receives [PaddingValues] for title bar and bottom bar insets.
9397
*/
9498
@Composable
@@ -113,6 +117,7 @@ fun Scaffold(
113117
bottomBar: (@Composable () -> Unit)? = null,
114118
bottomBarHeight: Int = 38,
115119
pushContent: Boolean = false,
120+
dismissPanelsOnContentTap: Boolean = false,
116121
content: @Composable (PaddingValues) -> Unit,
117122
) {
118123
// --- Resolve effective column visibility ---
@@ -218,8 +223,29 @@ fun Scaffold(
218223

219224
@Composable
220225
fun ContentArea(contentModifier: Modifier) {
226+
val showInlineInspector = !pushContent && inspector != null
227+
val animatedInspectorWidth by animateDpAsState(
228+
targetValue = if (showInlineInspector && inspectorVisible) currentInspectorWidth else 0.dp,
229+
animationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing),
230+
)
231+
val separatorColor = if (isDark) Color.White.copy(alpha = 0.10f) else Color.Black.copy(alpha = 0.10f)
232+
221233
Box(
222-
modifier = contentModifier,
234+
modifier = contentModifier
235+
.clipToBounds()
236+
.drawWithContent {
237+
drawContent()
238+
// Inspector separator drawn on top of everything (including title bar glass)
239+
if (animatedInspectorWidth > 0.dp) {
240+
val x = size.width - animatedInspectorWidth.toPx()
241+
drawLine(
242+
color = separatorColor,
243+
start = Offset(x, 0f),
244+
end = Offset(x, size.height),
245+
strokeWidth = 1.dp.toPx(),
246+
)
247+
}
248+
},
223249
) {
224250
// Content layer captured for title bar glass
225251
Box(
@@ -323,56 +349,50 @@ fun Scaffold(
323349
}
324350
}
325351

326-
// ---- Inspector (right panel) ----
327-
if (inspector != null) {
352+
// ---- Inspector (resize mode, non-push only) ----
353+
if (showInlineInspector) {
354+
val inspectorSizeTween = tween<IntSize>(durationMillis = 250, easing = FastOutSlowInEasing)
328355
AnimatedVisibility(
329356
visible = inspectorVisible,
330357
enter = expandHorizontally(
331-
animationSpec = macosSpring(SpringPreset.Snappy),
358+
animationSpec = inspectorSizeTween,
332359
expandFrom = Alignment.End,
333360
),
334361
exit = shrinkHorizontally(
335-
animationSpec = macosSpring(SpringPreset.Snappy),
362+
animationSpec = inspectorSizeTween,
336363
shrinkTowards = Alignment.End,
337364
),
338365
) {
339-
Row {
340-
// Inspector divider
341-
if (inspectorWidth is ColumnWidth.Flexible) {
342-
val flex = inspectorWidth
343-
ColumnDivider(
344-
onDrag = { delta ->
345-
currentInspectorWidth = (currentInspectorWidth - delta)
346-
.coerceIn(flex.min, flex.max)
347-
},
348-
onReset = { currentInspectorWidth = flex.ideal },
349-
)
350-
}
351-
352-
Box(
353-
modifier = Modifier
354-
.width(currentInspectorWidth)
355-
.fillMaxHeight()
356-
.padding(top = topPadding)
357-
.background(MacosTheme.colorScheme.surface)
358-
.drawBehind {
359-
// Left border
360-
drawLine(
361-
color = borderColor,
362-
start = Offset(0f, 0f),
363-
end = Offset(0f, size.height),
364-
strokeWidth = 1.dp.toPx(),
365-
)
366-
},
367-
) {
368-
inspector()
369-
}
366+
Box(
367+
modifier = Modifier
368+
.width(currentInspectorWidth)
369+
.fillMaxHeight(),
370+
) {
371+
inspector()
370372
}
371373
}
372374
}
373375
}
374376
}
375377

378+
// ---- Inspector separator (non-push only, outside liquefiable) ----
379+
if (showInlineInspector) {
380+
if (inspectorWidth is ColumnWidth.Flexible) {
381+
val flex = inspectorWidth
382+
ColumnDivider(
383+
modifier = Modifier
384+
.align(Alignment.TopEnd)
385+
.fillMaxHeight()
386+
.offset { IntOffset(-animatedInspectorWidth.roundToPx(), 0) },
387+
onDrag = { delta ->
388+
currentInspectorWidth = (currentInspectorWidth - delta)
389+
.coerceIn(flex.min, flex.max)
390+
},
391+
onReset = { currentInspectorWidth = flex.ideal },
392+
)
393+
}
394+
}
395+
376396
// ---- Title bar overlay (over content area only) ----
377397
if (titleBar != null || managedToggle) {
378398
val titleBarGlassModifier = Modifier.liquidGlassFade(
@@ -415,10 +435,12 @@ fun Scaffold(
415435
}
416436
}
417437
}
438+
418439
}
419440
}
420441

421442
if (pushContent && sidebar != null) {
443+
val pushSeparatorColor = if (isDark) Color.White.copy(alpha = 0.10f) else Color.Black.copy(alpha = 0.10f)
422444
BoxWithConstraints(
423445
modifier = modifier.fillMaxSize().background(containerColor).clipToBounds(),
424446
) {
@@ -427,13 +449,38 @@ fun Scaffold(
427449
targetValue = if (showSidebar) 0.dp else -currentSidebarWidth,
428450
animationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing),
429451
)
430-
// Content: full width, pushed right when sidebar is shown
431-
ContentArea(
432-
Modifier
452+
val inspectorOffset by animateDpAsState(
453+
targetValue = if (inspectorVisible && inspector != null) 0.dp else currentInspectorWidth,
454+
animationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing),
455+
)
456+
// Content: full width, pushed right by sidebar and left by inspector
457+
Box(
458+
modifier = Modifier
433459
.width(parentWidth)
434460
.fillMaxHeight()
435-
.offset { IntOffset((sidebarOffset + currentSidebarWidth).roundToPx(), 0) },
436-
)
461+
.offset { IntOffset(
462+
((sidebarOffset + currentSidebarWidth) - (currentInspectorWidth - inspectorOffset)).roundToPx(),
463+
0,
464+
) },
465+
) {
466+
ContentArea(Modifier.fillMaxSize())
467+
468+
// Dismiss overlay: tap content to close sidebar / inspector
469+
if (dismissPanelsOnContentTap && (showSidebar || (inspectorVisible && inspector != null))) {
470+
Box(
471+
modifier = Modifier
472+
.fillMaxSize()
473+
.zIndex(10f)
474+
.clickable(
475+
interactionSource = remember { MutableInteractionSource() },
476+
indication = null,
477+
) {
478+
if (showSidebar && managedToggle) toggleSidebar()
479+
if (inspectorVisible) onInspectorVisibleChange?.invoke(false)
480+
},
481+
)
482+
}
483+
}
437484
// Sidebar: slides in from the left, driven by same sidebarOffset
438485
val sidebarResizeCallbacks = if (sidebarWidth is ColumnWidth.Flexible) {
439486
SidebarResizeCallbacks(
@@ -461,6 +508,27 @@ fun Scaffold(
461508
sidebar()
462509
}
463510
}
511+
// Inspector: slides in from the right, pushing content left
512+
if (inspector != null) {
513+
Box(
514+
modifier = Modifier
515+
.width(currentInspectorWidth)
516+
.fillMaxHeight()
517+
.align(Alignment.TopEnd)
518+
.offset { IntOffset(inspectorOffset.roundToPx(), 0) }
519+
.drawWithContent {
520+
drawContent()
521+
drawLine(
522+
color = pushSeparatorColor,
523+
start = Offset(0f, 0f),
524+
end = Offset(0f, size.height),
525+
strokeWidth = 1.dp.toPx(),
526+
)
527+
},
528+
) {
529+
inspector()
530+
}
531+
}
464532
}
465533
} else {
466534
Row(modifier = modifier.fillMaxSize().background(containerColor)) {

macosui/src/commonMain/kotlin/io/github/kdroidfilter/nucleus/ui/apple/macos/components/TitleBar.kt

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ fun TitleBar(
106106
forcedColorScheme: ColorScheme? = null,
107107
navigationActions: @Composable RowScope.() -> Unit = {},
108108
title: @Composable () -> Unit = {},
109+
titleAlignment: Alignment = Alignment.Center,
109110
actions: @Composable RowScope.() -> Unit = {},
110111
backgroundColor: Color = MacosTheme.colorScheme.background,
111112
showBottomBorder: Boolean = false,
@@ -122,6 +123,7 @@ fun TitleBar(
122123
backgroundStyle = backgroundStyle,
123124
navigationActions = navigationActions,
124125
title = title,
126+
titleAlignment = titleAlignment,
125127
actions = actions,
126128
backgroundColor = backgroundColor,
127129
showBottomBorder = showBottomBorder,
@@ -149,6 +151,7 @@ private fun TitleBarContent(
149151
backgroundStyle: TitleBarBackground,
150152
navigationActions: @Composable RowScope.() -> Unit,
151153
title: @Composable () -> Unit,
154+
titleAlignment: Alignment = Alignment.Center,
152155
actions: @Composable RowScope.() -> Unit,
153156
backgroundColor: Color,
154157
showBottomBorder: Boolean,
@@ -254,18 +257,20 @@ private fun TitleBarContent(
254257
.weight(1f)
255258
.fillMaxHeight()
256259
.graphicsLayer { alpha = if (showsTitle) 1f else 0f },
257-
contentAlignment = Alignment.Center,
260+
contentAlignment = titleAlignment,
258261
) {
259262
title()
260263
}
261264
}
262265

263-
// Right section (actions): anchored at end, measured to drive push offset
266+
// Right section (actions): anchored at end, unbounded so expanding
267+
// children (e.g. search field) push the left section instead of compressing
264268
Row(
265269
verticalAlignment = Alignment.CenterVertically,
266270
horizontalArrangement = Arrangement.spacedBy(style.actionSpacing, Alignment.End),
267271
modifier = Modifier
268272
.align(Alignment.CenterEnd)
273+
.wrapContentWidth(unbounded = true, align = Alignment.End)
269274
.padding(start = 8.dp)
270275
.widthIn(min = style.actionsMinWidth)
271276
.onGloballyPositioned { actionsWidthPx = it.size.width },
@@ -292,7 +297,7 @@ private fun TitleBarContent(
292297
.weight(1f)
293298
.fillMaxHeight()
294299
.graphicsLayer { alpha = if (showsTitle) 1f else 0f },
295-
contentAlignment = Alignment.Center,
300+
contentAlignment = titleAlignment,
296301
) {
297302
title()
298303
}

0 commit comments

Comments
 (0)