Fluid 1.3.0
Material Design for QtQuick and Qml
Loading...
Searching...
No Matches
SearchAppBar.qml
Go to the documentation of this file.
1// SPDX-FileCopyrightText: 2026 Pier Luigi Fiorini <pierluigi.fiorini@gmail.com>
2// SPDX-License-Identifier: MPL-2.0
3
4pragma ComponentBehavior: Bound
5
6import QtQuick
7import QtQuick.Templates as T
8import Fluid as MD
9import "../core/UiMetrics.js" as UiMetrics
10import "../internal/MotionAnimation.js" as MotionAnimation
11
49MD.BaseAppBar {
50 id: control
51
52 Accessible.role: Accessible.ToolBar
53 Accessible.name: qsTr("Search")
54
55
62 enum Mode {
63 Launcher,
64 Editable
65 }
66
73 enum TextAlignment {
74 Start,
75 Center
76 }
77
79 property int mode: SearchAppBar.Launcher
80
87 property string text
88
90 property string placeholderText: qsTr("Search")
91
92
93 property int textAlignment: SearchAppBar.Start
94
100 property int inputMethodHints: Qt.ImhNone
101
107 property var validator: null
108
115 property MD.AppBarAction searchNavigationAction: defaultSearchAction
116
118 property list<MD.AppBarAction> searchActions
119
121 property color searchContainerColor: MD.Style.surfaceContainerColor
122
124 property color scrolledSearchContainerColor: MD.Style.surfaceContainerHighestColor
125
127 property color searchTextColor: MD.Style.onSurfaceColor
128
130 property color searchContentColor: MD.Style.onSurfaceVariantColor
131
133 signal activated
134
136 signal accepted
137
139 readonly property bool _centeredText: textAlignment !== 0
140
142 property var _visibleSearchActions: []
143
145 property var _searchOverflowActions: []
146
148 property real _capsuleWidth: 0
149
151 property var _searchFieldItem: null
152
154 property var _searchActionRepeater: null
155
156 collapsedHeight: MD.Tokens.appBar.smallContainerHeight
157 expandedHeight: collapsedHeight
158 scrollBehavior: MD.BaseAppBar.EnterAlways
159 minimumCenterWidth: MD.Tokens.appBar.searchContainerHeight + MD.Tokens.appBar.searchOuterFieldMargin * 2
160 supplementalOverflowActions: _searchOverflowActions
161
163 function _searchActionWidth(index) {
164 const item = _searchActionRepeater ? _searchActionRepeater.itemAt(index) : null;
165 return item ? item.implicitWidth : MD.Tokens.appBar.minimumInteractiveSize;
166 }
167
169 function _reflowSearchActions() {
170 const candidates = [];
171 const forcedVisible = [];
172 let usedWidth = 0;
173
174 for (let index = 0; index < searchActions.length; ++index) {
175 const action = searchActions[index];
176 if (!action || !action.visible)
177 continue;
178 const entry = {
179 action: action,
180 index: index,
181 width: _searchActionWidth(index)
182 };
183 if (action.overflowPolicy === MD.AppBarAction.NeverOverflow) {
184 forcedVisible.push(entry);
185 usedWidth += entry.width;
186 } else if (action.overflowPolicy !== MD.AppBarAction.AlwaysOverflow) {
187 candidates.push(entry);
188 }
189 }
190
191 const leadingWidth = searchNavigationAction && searchNavigationAction.visible ? MD.Tokens.appBar.minimumInteractiveSize + MD.Tokens.appBar.searchContainedLeadingSpace : MD.Tokens.appBar.searchContainedNoActionsLeadingSpace;
192 const fixedWidth = leadingWidth + MD.Tokens.appBar.searchContainedTrailingSpace + MD.Tokens.appBar.titleInset * 2;
193 const budget = Math.max(0, _capsuleWidth - fixedWidth);
194
195 candidates.sort((left, right) => {
196 if (left.action.priority !== right.action.priority)
197 return right.action.priority - left.action.priority;
198 return left.index - right.index;
199 });
200
201 const selected = new Set();
202 for (const entry of forcedVisible)
203 selected.add(entry.action);
204 for (const entry of candidates) {
205 if (usedWidth + entry.width <= budget) {
206 selected.add(entry.action);
207 usedWidth += entry.width;
208 }
209 }
210
211 const visible = [];
212 const overflow = [];
213 for (const action of searchActions) {
214 if (!action || !action.visible)
215 continue;
216 if (action.overflowPolicy === MD.AppBarAction.AlwaysOverflow || (action.overflowPolicy === MD.AppBarAction.AutoOverflow && !selected.has(action))) {
217 overflow.push(action);
218 } else {
219 visible.push(action);
220 }
221 }
222 _visibleSearchActions = visible;
223 _searchOverflowActions = overflow;
224 }
225
227 function _scheduleSearchReflow() {
228 _reflowSearchActions();
229 Qt.callLater(() => control._reflowSearchActions());
230 }
231
237 function forceSearchFocus() {
238 if (mode === SearchAppBar.Editable && _searchFieldItem)
239 _searchFieldItem.forceActiveFocus();
240 }
241
243 function clear() {
244 text = "";
245 }
246
247 onSearchActionsChanged: _scheduleSearchReflow()
248 onSearchNavigationActionChanged: _scheduleSearchReflow()
249 on_CapsuleWidthChanged: _scheduleSearchReflow()
250 onModeChanged: {
251 if (mode === SearchAppBar.Editable && _searchFieldItem)
252 Qt.callLater(() => control.forceSearchFocus());
253 }
254
255 Component.onCompleted: _scheduleSearchReflow()
256
257 MD.AppBarAction {
258 id: defaultSearchAction
259
260 text: qsTr("Search")
261 icon.name: MD.SymbolNames.symbolSearch
262 overflowPolicy: MD.AppBarAction.NeverOverflow
263 onTriggered: control.activated()
264 }
265
266 centerContent: Component {
267 Item {
268 id: searchArea
269
270 readonly property real physicalLeadingWidth: control.layoutMirrored ? control.trailingWidth : control.leadingWidth
271 readonly property real physicalTrailingWidth: control.layoutMirrored ? control.leadingWidth : control.trailingWidth
272 readonly property real regionLeft: physicalLeadingWidth + MD.Tokens.appBar.searchOuterFieldMargin
273 readonly property real regionRight: width - physicalTrailingWidth - MD.Tokens.appBar.searchOuterFieldMargin
274 readonly property real availableWidth: Math.max(0, regionRight - regionLeft)
275 readonly property real desiredWidth: availableWidth <= MD.Tokens.appBar.searchAdaptiveBreakpoint ? availableWidth : Math.max(MD.Tokens.appBar.searchAdaptiveBreakpoint, availableWidth * MD.Tokens.appBar.searchAdaptiveWidthFraction)
276 readonly property real capsuleX: Math.max(regionLeft, Math.min((width - desiredWidth) / 2, regionRight - desiredWidth))
277
278 Binding {
279 target: control
280 property: "_capsuleWidth"
281 value: capsule.width
282 }
283
284 Rectangle {
285 id: capsule
286 objectName: "searchCapsule"
287
288 x: searchArea.capsuleX
289 y: (control.collapsedHeight - height) / 2
290 width: Math.max(0, Math.min(searchArea.availableWidth, searchArea.desiredWidth))
291 height: MD.Tokens.appBar.searchContainerHeight
292 topLeftRadius: UiMetrics.resolveShapeRadius(MD.Tokens.shape.cornerFull.topLeft,
293 width, height)
294 topRightRadius: UiMetrics.resolveShapeRadius(MD.Tokens.shape.cornerFull.topRight,
295 width, height)
296 bottomLeftRadius: UiMetrics.resolveShapeRadius(
297 MD.Tokens.shape.cornerFull.bottomLeft, width, height)
298 bottomRightRadius: UiMetrics.resolveShapeRadius(
299 MD.Tokens.shape.cornerFull.bottomRight, width, height)
300 color: control.lifted ? control.scrolledSearchContainerColor : control.searchContainerColor
301 clip: true
302
303 Behavior on color {
304 ColorAnimation {
305 duration: MotionAnimation.expressiveFastEffectsDuration
306 easing.type: Easing.BezierSpline
307 easing.bezierCurve: MotionAnimation.expressiveFastEffectsCurve
308 }
309 }
310
311 T.AbstractButton {
312 id: launcherButton
313 objectName: "searchLauncher"
314
315 anchors.fill: parent
316 text: control.text.length > 0 ? control.text : control.placeholderText
317 visible: control.mode === SearchAppBar.Launcher
318 hoverEnabled: true
319 focusPolicy: Qt.StrongFocus
320 background: Item {}
321
322 Accessible.role: Accessible.Button
323 Accessible.name: text
324
325 onClicked: control.activated()
326 Keys.onPressed: event => {
327 if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_Space) {
328 control.activated();
329 event.accepted = true;
330 }
331 }
332 }
333
334 MD.AppBarActionDelegate {
335 id: searchNavigationButton
336 objectName: "searchNavigationActionButton"
337
338 anchors.left: parent.left
339 anchors.verticalCenter: parent.verticalCenter
340 actionData: control.searchNavigationAction || defaultSearchAction
341 contentColor: control.searchContentColor
342 visible: control.searchNavigationAction !== null && control.searchNavigationAction.visible
343 accessibilityIgnored: control.mode === SearchAppBar.Launcher
344 && control.searchNavigationAction === defaultSearchAction
345 z: 2
346 }
347
348 Row {
349 id: searchTrailingRow
350 objectName: "searchTrailingActions"
351
352 anchors.right: parent.right
353 anchors.verticalCenter: parent.verticalCenter
354 spacing: MD.Tokens.appBar.searchTrailingActionsGap
355 z: 2
356
357 Repeater {
358 id: searchActionRepeater
359 Component.onCompleted: {
360 control._searchActionRepeater = searchActionRepeater;
361 control._scheduleSearchReflow();
362 }
363 Component.onDestruction: {
364 if (control._searchActionRepeater === searchActionRepeater)
365 control._searchActionRepeater = null;
366 }
367
368 model: control.searchActions
369
370 delegate: MD.AppBarActionDelegate {
371 id: searchActionDelegate
372 required property var modelData
373
374 actionData: modelData
375 contentColor: control.searchContentColor
376 visible: control._visibleSearchActions.indexOf(actionData) >= 0
377 onImplicitWidthChanged: control._scheduleSearchReflow()
378
379 Connections {
380 target: searchActionDelegate.modelData
381 function onVisibleChanged() {
382 control._scheduleSearchReflow();
383 }
384 function onPriorityChanged() {
385 control._scheduleSearchReflow();
386 }
387 function onOverflowPolicyChanged() {
388 control._scheduleSearchReflow();
389 }
390 function onPresentationChanged() {
391 control._scheduleSearchReflow();
392 }
393 function onTextChanged() {
394 control._scheduleSearchReflow();
395 }
396 }
397 }
398 }
399 }
400
401 readonly property real physicalStartWidth: control.layoutMirrored ? searchTrailingRow.width : searchNavigationButton.width
402 readonly property real physicalEndWidth: control.layoutMirrored ? searchNavigationButton.width : searchTrailingRow.width
403 readonly property real textLeft: physicalStartWidth + (searchNavigationButton.visible ? MD.Tokens.appBar.searchContainedLeadingSpace : MD.Tokens.appBar.searchContainedNoActionsLeadingSpace)
404 readonly property real textRight: width - physicalEndWidth - (searchTrailingRow.width > 0 ? MD.Tokens.appBar.searchContainedTrailingSpace : MD.Tokens.appBar.searchContainedNoActionsTrailingSpace)
405
406 MD.Label {
407 id: launcherLabel
408 objectName: "searchLauncherLabel"
409
410 x: capsule.textLeft
411 y: 0
412 width: Math.max(0, capsule.textRight - capsule.textLeft)
413 height: parent.height
414 text: control.text.length > 0 ? control.text : control.placeholderText
415 typescale: MD.Tokens.typescale.bodyLarge
416 color: control.searchTextColor
417 opacity: control.text.length > 0 ? 1 : MD.Tokens.appBar.disabledContentOpacity
418 horizontalAlignment: control._centeredText ? Text.AlignHCenter : control.layoutMirrored ? Text.AlignRight : Text.AlignLeft
419 verticalAlignment: Text.AlignVCenter
420 maximumLineCount: 1
421 elide: Text.ElideRight
422 visible: control.mode === SearchAppBar.Launcher
423 Accessible.ignored: true
424 }
425
426 T.TextField {
427 id: searchFieldProxy
428 Component.onCompleted: control._searchFieldItem = searchFieldProxy
429 Component.onDestruction: {
430 if (control._searchFieldItem === searchFieldProxy)
431 control._searchFieldItem = null;
432 }
433 objectName: "searchField"
434
435 x: capsule.textLeft
436 y: 0
437 width: Math.max(0, capsule.textRight - capsule.textLeft)
438 height: parent.height
439 text: control.text
440 placeholderText: control.placeholderText
441 inputMethodHints: control.inputMethodHints
442 validator: control.validator
443 color: control.searchTextColor
444 placeholderTextColor: control.searchContentColor
445 selectionColor: control.MD.Style.primaryColor
446 selectedTextColor: control.MD.Style.onPrimaryColor
447 font.family: control.MD.Style.plainFontFamily
448 font.pixelSize: MD.Tokens.typescale.bodyLarge.fontSize
449 font.weight: MD.Tokens.typescale.bodyLarge.fontWeight
450 font.letterSpacing: MD.Tokens.typescale.bodyLarge.tracking
451 horizontalAlignment: control._centeredText ? Text.AlignHCenter : control.layoutMirrored ? Text.AlignRight : Text.AlignLeft
452 verticalAlignment: Text.AlignVCenter
453 leftPadding: 0
454 rightPadding: 0
455 topPadding: 0
456 bottomPadding: 0
457 focusPolicy: Qt.StrongFocus
458 visible: control.mode === SearchAppBar.Editable
459 background: Item {}
460
461 Accessible.role: Accessible.EditableText
462 Accessible.name: control.placeholderText
463 Accessible.searchEdit: true
464
465 onTextEdited: control.text = text
466 onAccepted: control.accepted()
467 }
468
469 TapHandler {
470 enabled: control.mode === SearchAppBar.Editable
471 gesturePolicy: TapHandler.ReleaseWithinBounds
472 onTapped: searchFieldProxy.forceActiveFocus()
473 }
474 }
475 }
476 }
477}
Describes an action shown by an AppBar or SearchAppBar.
Material Design 3 design tokens.
Definition tokens.h:65
Tokens(QObject *parent=nullptr)
Definition tokens.cpp:8
Displays text using a selectable Material 3 type scale.
Definition Label.qml:15
A Material 3 Expressive app bar containing an adaptive search field.
Material Design 3 Expressive button tokens.
Definition button.h:21