Skip to content

Commit 78f3e57

Browse files
committed
feat(titlebar): add pushActionsOnExpand mode for mobile search expansion
- Add navigationActionsMinWidth param to TitleBar for flexible left section sizing - Add pushActionsOnExpand param to conditionally apply push behavior - When enabled, search field expansion pushes title left with clip (mobile) - When disabled, original reflow behavior preserved (desktop) - Also update App.kt to use pushActionsOnExpand on compact screens
1 parent 2f7e545 commit 78f3e57

2 files changed

Lines changed: 121 additions & 68 deletions

File tree

  • macosui/src/commonMain/kotlin/io/github/kdroidfilter/nucleus/ui/apple/macos/components
  • sample/src/commonMain/kotlin/io/github/kdroidfilter/nucleus/ui/apple/macos/sample

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

Lines changed: 120 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import androidx.compose.runtime.*
1414
import androidx.compose.ui.Alignment
1515
import androidx.compose.ui.Modifier
1616
import androidx.compose.ui.draw.clip
17+
import androidx.compose.ui.draw.clipToBounds
1718
import androidx.compose.ui.draw.drawBehind
1819
import androidx.compose.ui.geometry.Offset
1920
import androidx.compose.ui.graphics.Color
@@ -111,6 +112,7 @@ fun TitleBar(
111112
glass: Boolean = false,
112113
height: Int = style.height,
113114
navigationActionsMinWidth: Dp = style.navMinWidth,
115+
pushActionsOnExpand: Boolean = false,
114116
) {
115117
val content = @Composable {
116118
TitleBarContent(
@@ -126,6 +128,7 @@ fun TitleBar(
126128
glass = glass,
127129
height = height,
128130
navigationActionsMinWidth = navigationActionsMinWidth,
131+
pushActionsOnExpand = pushActionsOnExpand,
129132
)
130133
}
131134

@@ -152,6 +155,7 @@ private fun TitleBarContent(
152155
glass: Boolean,
153156
height: Int,
154157
navigationActionsMinWidth: Dp = style.navMinWidth,
158+
pushActionsOnExpand: Boolean = false,
155159
) {
156160
val isDark = MacosTheme.colorScheme.isDark
157161
val borderColor = if (isDark) Color.Black.copy(alpha = 0.5f) else Color.Black.copy(alpha = 0.1f)
@@ -176,83 +180,131 @@ private fun TitleBarContent(
176180

177181
val onTitleBarDoubleClick = LocalTitleBarDoubleClick.current
178182

183+
val outerModifier = modifier
184+
.fillMaxWidth()
185+
.height(height.dp)
186+
.then(
187+
if (onTitleBarDoubleClick != null) {
188+
Modifier.pointerInput(onTitleBarDoubleClick) {
189+
awaitPointerEventScope {
190+
var lastPressTime = 0L
191+
while (true) {
192+
val event = awaitPointerEvent(PointerEventPass.Final)
193+
if (event.type == PointerEventType.Press &&
194+
event.changes.none { it.isConsumed }
195+
) {
196+
val now = event.changes.first().uptimeMillis
197+
if (now - lastPressTime < viewConfiguration.doubleTapTimeoutMillis) {
198+
onTitleBarDoubleClick()
199+
lastPressTime = 0L
200+
} else {
201+
lastPressTime = now
202+
}
203+
}
204+
}
205+
}
206+
}
207+
} else {
208+
Modifier
209+
},
210+
)
211+
.then(bgModifier)
212+
.then(
213+
if (showBorder) {
214+
Modifier.drawBehind {
215+
drawLine(
216+
color = borderColor,
217+
start = Offset(0f, size.height),
218+
end = Offset(size.width, size.height),
219+
strokeWidth = 0.5.dp.toPx(),
220+
)
221+
}
222+
} else {
223+
Modifier
224+
},
225+
)
226+
179227
CompositionLocalProvider(
180228
LocalTitleBarStyle provides style,
181229
LocalTextStyle provides titleTextStyle,
182230
) {
183-
Row(
184-
modifier = modifier
185-
.fillMaxWidth()
186-
.height(height.dp)
187-
.then(
188-
if (onTitleBarDoubleClick != null) {
189-
Modifier.pointerInput(onTitleBarDoubleClick) {
190-
awaitPointerEventScope {
191-
var lastPressTime = 0L
192-
while (true) {
193-
val event = awaitPointerEvent(PointerEventPass.Final)
194-
if (event.type == PointerEventType.Press &&
195-
event.changes.none { it.isConsumed }
196-
) {
197-
val now = event.changes.first().uptimeMillis
198-
if (now - lastPressTime < viewConfiguration.doubleTapTimeoutMillis) {
199-
onTitleBarDoubleClick()
200-
lastPressTime = 0L
201-
} else {
202-
lastPressTime = now
203-
}
204-
}
205-
}
206-
}
207-
}
208-
} else {
209-
Modifier
210-
},
211-
)
212-
.then(bgModifier)
213-
.then(
214-
if (showBorder) {
215-
Modifier.drawBehind {
216-
drawLine(
217-
color = borderColor,
218-
start = Offset(0f, size.height),
219-
end = Offset(size.width, size.height),
220-
strokeWidth = 0.5.dp.toPx(),
221-
)
222-
}
223-
} else {
224-
Modifier
225-
},
226-
)
227-
.padding(horizontal = style.horizontalPadding),
228-
verticalAlignment = Alignment.CenterVertically,
229-
) {
230-
// Left section: Nav Actions
231-
Row(
232-
verticalAlignment = Alignment.CenterVertically,
233-
modifier = Modifier.padding(end = 8.dp).widthIn(min = navigationActionsMinWidth),
234-
) {
235-
navigationActions()
236-
}
231+
if (pushActionsOnExpand) {
232+
val density = LocalDensity.current
233+
var actionsWidthPx by remember { mutableStateOf(0) }
234+
val actionsMinWidthPx = remember(style, density) { with(density) { style.actionsMinWidth.roundToPx() } }
235+
val pushOffsetPx = (actionsWidthPx - actionsMinWidthPx).coerceAtLeast(0)
236+
237+
Box(modifier = outerModifier.clipToBounds().padding(horizontal = style.horizontalPadding)) {
238+
// Left section (nav + title): fills full width, pushed left as actions expand
239+
Row(
240+
modifier = Modifier
241+
.fillMaxWidth()
242+
.fillMaxHeight()
243+
.offset { IntOffset(-pushOffsetPx, 0) },
244+
verticalAlignment = Alignment.CenterVertically,
245+
) {
246+
Row(
247+
verticalAlignment = Alignment.CenterVertically,
248+
modifier = Modifier.padding(end = 8.dp).widthIn(min = navigationActionsMinWidth),
249+
) {
250+
navigationActions()
251+
}
252+
Box(
253+
modifier = Modifier
254+
.weight(1f)
255+
.fillMaxHeight()
256+
.graphicsLayer { alpha = if (showsTitle) 1f else 0f },
257+
contentAlignment = Alignment.Center,
258+
) {
259+
title()
260+
}
261+
}
237262

238-
// Center section: Title or Search
239-
Box(
240-
modifier = Modifier
241-
.weight(1f)
242-
.fillMaxHeight()
243-
.graphicsLayer { alpha = if (showsTitle) 1f else 0f },
244-
contentAlignment = Alignment.Center,
245-
) {
246-
title()
263+
// Right section (actions): anchored at end, measured to drive push offset
264+
Row(
265+
verticalAlignment = Alignment.CenterVertically,
266+
horizontalArrangement = Arrangement.spacedBy(style.actionSpacing, Alignment.End),
267+
modifier = Modifier
268+
.align(Alignment.CenterEnd)
269+
.padding(start = 8.dp)
270+
.widthIn(min = style.actionsMinWidth)
271+
.onGloballyPositioned { actionsWidthPx = it.size.width },
272+
) {
273+
actions()
274+
}
247275
}
248-
249-
// Right section: Actions
276+
} else {
250277
Row(
278+
modifier = outerModifier.padding(horizontal = style.horizontalPadding),
251279
verticalAlignment = Alignment.CenterVertically,
252-
horizontalArrangement = Arrangement.spacedBy(style.actionSpacing, Alignment.End),
253-
modifier = Modifier.padding(start = 8.dp).widthIn(min = style.actionsMinWidth),
254280
) {
255-
actions()
281+
// Left section: Nav Actions
282+
Row(
283+
verticalAlignment = Alignment.CenterVertically,
284+
modifier = Modifier.padding(end = 8.dp).widthIn(min = navigationActionsMinWidth),
285+
) {
286+
navigationActions()
287+
}
288+
289+
// Center section: Title
290+
Box(
291+
modifier = Modifier
292+
.weight(1f)
293+
.fillMaxHeight()
294+
.graphicsLayer { alpha = if (showsTitle) 1f else 0f },
295+
contentAlignment = Alignment.Center,
296+
) {
297+
title()
298+
}
299+
300+
// Right section: Actions
301+
Row(
302+
verticalAlignment = Alignment.CenterVertically,
303+
horizontalArrangement = Arrangement.spacedBy(style.actionSpacing, Alignment.End),
304+
modifier = Modifier.padding(start = 8.dp).widthIn(min = style.actionsMinWidth),
305+
) {
306+
actions()
307+
}
256308
}
257309
}
258310
}

sample/src/commonMain/kotlin/io/github/kdroidfilter/nucleus/ui/apple/macos/sample/App.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,7 @@ fun App() {
312312
titleBar = {
313313
TitleBar(
314314
glass = true,
315+
pushActionsOnExpand = isCompact,
315316
navigationActionsMinWidth = if (isCompact) 0.dp else 80.dp,
316317
navigationActions = if (!isCompact) {
317318
{

0 commit comments

Comments
 (0)