Fluid 1.3.0
Material Design for QtQuick and Qml
Loading...
Searching...
No Matches
Menu.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 Fluid as MD
7import "../core/UiMetrics.js" as UiMetrics
8import "../internal/MotionAnimation.js" as MotionAnimation
9
10import QtQuick
11import QtQuick.Templates as T
12import QtQuick.Window
13
80T.Menu {
81 id: control
82
84 enum Variant {
86 Vertical,
88 Baseline
89 }
90
92 enum ColorStyle {
94 Standard,
96 Vibrant
97 }
98
105 property int variant: Menu.Vertical
106
112 property int colorStyle: Menu.Standard
113
115 readonly property real _viewportWidth: parent && parent.Window.window && parent.Window.window.width > 0
116 ? parent.Window.window.width - leftMargin - rightMargin
117 : MD.Tokens.menu.maximumWidth
118
120 readonly property real _viewportHeight: parent && parent.Window.window && parent.Window.window.height > 0
121 ? parent.Window.window.height - topMargin - bottomMargin
122 : Number.POSITIVE_INFINITY
123
125 readonly property bool _layoutMirrored: mirrored || locale.textDirection === Qt.RightToLeft || (parent && parent.LayoutMirroring.enabled)
126
128 property var _surfaceGroups: []
129
131 function _updateSurfaceGroups() {
132 if (variant !== Menu.Vertical) {
133 _surfaceGroups = [];
134 return;
135 }
136 const groups = [];
137 let firstItem = null;
138 let lastItem = null;
139 for (let index = 0; index < count; ++index) {
140 const item = itemAt(index);
141 if (!item)
142 continue;
143 if (_contentType(item) === 2) {
144 if (firstItem)
145 groups.push({ first: firstItem, last: lastItem });
146 firstItem = null;
147 lastItem = null;
148 } else {
149 if (!firstItem)
150 firstItem = item;
151 lastItem = item;
152 }
153 }
154 if (firstItem)
155 groups.push({ first: firstItem, last: lastItem });
156 _surfaceGroups = groups;
157 }
158
160 function _scheduleSurfaceUpdate() {
161 Qt.callLater(() => {
162 if (control)
163 control._updateSurfaceGroups();
164 });
165 }
166
168 function _contentType(item) {
169 return item && item._menuContentType !== undefined ? item._menuContentType : -1;
170 }
171
173 function _isItemGroupBoundary(item) {
174 const contentType = _contentType(item);
175 return contentType === 1 || contentType === 2;
176 }
177
179 function _itemGroupPosition(menuIndex) {
180 if (menuIndex < 0)
181 return MD.MenuItem.Only;
182
183 let firstAction = -1;
184 let lastAction = -1;
185 for (let index = menuIndex; index >= 0; --index) {
186 const item = itemAt(index);
187 if (_isItemGroupBoundary(item))
188 break;
189 if (_contentType(item) === 0)
190 firstAction = index;
191 }
192 for (let index = menuIndex; index < count; ++index) {
193 const item = itemAt(index);
194 if (_isItemGroupBoundary(item))
195 break;
196 if (_contentType(item) === 0)
197 lastAction = index;
198 }
199
200 if (firstAction < 0 || firstAction === lastAction)
201 return MD.MenuItem.Only;
202 if (menuIndex === firstAction)
203 return MD.MenuItem.First;
204 if (menuIndex === lastAction)
205 return MD.MenuItem.Last;
206 return MD.MenuItem.Middle;
207 }
208
210 function _surfaceShape(index, surfaceCount) {
211 if (surfaceCount <= 1)
212 return MD.Tokens.menu.verticalOnlyGroupShape;
213 if (index === 0)
214 return MD.Tokens.menu.verticalFirstGroupShape;
215 if (index === surfaceCount - 1)
216 return MD.Tokens.menu.verticalLastGroupShape;
217 return MD.Tokens.menu.verticalMiddleGroupShape;
218 }
219
221 function _surfaceY(index, group) {
222 return index === 0 ? 0 : topPadding + group.first.y;
223 }
224
226 function _surfaceHeight(index, group) {
227 return group.last.y + group.last.height - group.first.y
228 + (index === 0 ? topPadding : 0)
229 + (index === _surfaceGroups.length - 1 ? bottomPadding : 0);
230 }
231
232 onCountChanged: control._scheduleSurfaceUpdate()
233 onVariantChanged: control._updateSurfaceGroups()
234 onAboutToShow: control._updateSurfaceGroups()
235 onOpened: control._updateSurfaceGroups()
236 Component.onCompleted: control._scheduleSurfaceUpdate()
237
239 readonly property real _itemsImplicitWidth: {
240 let itemWidth = 0;
241 for (let index = 0; index < count; ++index) {
242 const item = itemAt(index);
243 if (item)
244 itemWidth = Math.max(itemWidth, item.implicitWidth);
245 }
246 return itemWidth;
247 }
248
250 readonly property real _itemsImplicitHeight: {
251 let itemHeight = 0;
252 for (let index = 0; index < count; ++index) {
253 const item = itemAt(index);
254 if (item)
255 itemHeight += item.implicitHeight;
256 }
257 return itemHeight + Math.max(0, count - 1) * menuList.spacing;
258 }
259
261 readonly property real _naturalWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, _itemsImplicitWidth + leftPadding + rightPadding)
262
264 readonly property real _naturalHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, _itemsImplicitHeight + topPadding + bottomPadding)
265
266 implicitWidth: Math.max(0, Math.min(_viewportWidth, Math.max(MD.Tokens.menu.minimumWidth, Math.min(MD.Tokens.menu.maximumWidth, _naturalWidth))))
267 implicitHeight: Math.max(0, Math.min(_viewportHeight, _naturalHeight))
268
269 margins: 0
270 leftMargin: variant === Menu.Vertical ? MD.Tokens.menu.horizontalViewportMargin : MD.Tokens.menu.viewportMargin
271 rightMargin: leftMargin
272 topMargin: variant === Menu.Vertical ? MD.Tokens.menu.verticalViewportMargin : MD.Tokens.menu.viewportMargin
273 bottomMargin: topMargin
274 topPadding: variant === Menu.Vertical ? MD.Tokens.menu.verticalGroupContentPadding : MD.Tokens.menu.topPadding
275 bottomPadding: variant === Menu.Vertical ? MD.Tokens.menu.verticalGroupContentPadding : MD.Tokens.menu.bottomPadding
276 leftPadding: variant === Menu.Vertical ? MD.Tokens.menu.verticalGroupPadding : 0
277 rightPadding: leftPadding
278 overlap: 0
279
280 modal: false
281 focus: true
282 closePolicy: T.Popup.CloseOnEscape | T.Popup.CloseOnPressOutside
283 transformOrigin: cascade ? (_layoutMirrored ? Item.TopRight : Item.TopLeft) : Item.Top
284
285 delegate: MD.MenuItem {
286 id: menuDelegate
287
289 readonly property MD.Action _materialAction: action instanceof MD.Action
290 ? (action as MD.Action) : null
291
293 readonly property int _menuIndex: {
294 for (let itemIndex = 0; itemIndex < control.count; ++itemIndex) {
295 if (control.itemAt(itemIndex) === menuDelegate)
296 return itemIndex;
297 }
298 return -1;
299 }
300
301 variant: control.variant === Menu.Vertical ? MD.MenuItem.Vertical : MD.MenuItem.Baseline
302 colorStyle: control.colorStyle === Menu.Vibrant ? MD.MenuItem.Vibrant : MD.MenuItem.Standard
303 groupPosition: control._itemGroupPosition(_menuIndex)
304 supportingText: _materialAction ? _materialAction.supportingText : ""
305 trailingText: _materialAction ? _materialAction.trailingText : ""
306 badgeContent: _materialAction ? _materialAction.badgeContent : ""
307 LayoutMirroring.enabled: control._layoutMirrored
308 LayoutMirroring.childrenInherit: true
309 }
310
311 contentItem: ListView {
312 id: menuList
313 objectName: "menuListView"
314
315 implicitWidth: control._itemsImplicitWidth
316 implicitHeight: control._itemsImplicitHeight
317 model: control.contentModel
318 currentIndex: control.currentIndex
319 interactive: contentHeight + control.topPadding + control.bottomPadding > control.height
320 boundsBehavior: Flickable.StopAtBounds
321 spacing: 0
322 clip: true
323 LayoutMirroring.enabled: control._layoutMirrored
324 LayoutMirroring.childrenInherit: true
325
326 onContentHeightChanged: {
327 if (control)
328 control._updateSurfaceGroups();
329 }
330
331 ScrollIndicator.vertical: MD.ScrollIndicator {}
332 }
333
334 background: MD.ElevationRectangle {
335 objectName: "menuBackground"
336
337 implicitWidth: MD.Tokens.menu.minimumWidth
338 readonly property var containerShape: control.variant === Menu.Vertical
339 ? MD.Tokens.menu.verticalContainerShape
340 : MD.Tokens.menu.containerShape
341
342 implicitHeight: control.variant === Menu.Vertical ? MD.Tokens.menu.verticalItemHeight : MD.Tokens.menu.itemHeight
343 color: control.variant === Menu.Vertical
344 ? "transparent"
345 : control.colorStyle === Menu.Vibrant
346 ? control.MD.Style.tertiaryContainerColor
347 : control.MD.Style.surfaceContainerColor
348 topLeftRadius: UiMetrics.resolveShapeRadius(containerShape.topLeft,
349 width, height)
350 topRightRadius: UiMetrics.resolveShapeRadius(containerShape.topRight,
351 width, height)
352 bottomLeftRadius: UiMetrics.resolveShapeRadius(containerShape.bottomLeft,
353 width, height)
354 bottomRightRadius: UiMetrics.resolveShapeRadius(containerShape.bottomRight,
355 width, height)
356 elevation: MD.Tokens.menu.containerElevation
357
358 Repeater {
359 model: control._surfaceGroups
360
361 delegate: MD.ElevationRectangle {
362 required property int index
363 required property var modelData
364
365 objectName: "menuGroupSurface" + index
366 readonly property var groupShape: control._surfaceShape(index,
367 control._surfaceGroups.length)
368
369 // Following surfaces overlay preceding surfaces at the seam.
370 z: index
371 x: 0
372 y: control._surfaceY(index, modelData)
373 width: parent.width
374 height: control._surfaceHeight(index, modelData)
375 color: control.colorStyle === Menu.Vibrant
376 ? control.MD.Style.tertiaryContainerColor
377 : control.MD.Style.surfaceContainerLowColor
378 topLeftRadius: UiMetrics.resolveShapeRadius(groupShape.topLeft, width, height)
379 topRightRadius: UiMetrics.resolveShapeRadius(groupShape.topRight, width, height)
380 bottomLeftRadius: UiMetrics.resolveShapeRadius(groupShape.bottomLeft, width, height)
381 bottomRightRadius: UiMetrics.resolveShapeRadius(groupShape.bottomRight, width, height)
382 elevation: MD.Tokens.menu.containerElevation
383 }
384 }
385 }
386
387 enter: Transition {
388 NumberAnimation {
389 property: "scale"
390 from: control.variant === Menu.Vertical ? MD.Tokens.menu.closedScale : 0.94
391 to: 1
392 duration: MotionAnimation.expressiveFastSpatialDuration
393 easing.type: Easing.BezierSpline
394 easing.bezierCurve: MotionAnimation.expressiveFastSpatialCurve
395 }
396 NumberAnimation {
397 property: "opacity"
398 from: 0
399 to: 1
400 duration: MotionAnimation.expressiveFastEffectsDuration
401 easing.type: Easing.BezierSpline
402 easing.bezierCurve: MotionAnimation.expressiveFastEffectsCurve
403 }
404 }
405
406 exit: Transition {
407 NumberAnimation {
408 property: "scale"
409 from: 1
410 to: control.variant === Menu.Vertical ? MD.Tokens.menu.closedScale : 0.94
411 duration: MotionAnimation.expressiveFastSpatialDuration
412 easing.type: Easing.BezierSpline
413 easing.bezierCurve: MotionAnimation.expressiveFastSpatialCurve
414 }
415 NumberAnimation {
416 property: "opacity"
417 from: 1
418 to: 0
419 duration: MotionAnimation.expressiveFastEffectsDuration
420 easing.type: Easing.BezierSpline
421 easing.bezierCurve: MotionAnimation.expressiveFastEffectsCurve
422 }
423 }
424}
Describes a reusable Fluid action.
Definition Action.qml:30
A selectable Material Design 3 menu item.
Definition MenuItem.qml:17
A non-interactive indicator of the visible portion of scrolling content.
Material Design 3 tokens for menus and menu items.
Definition menu.h:26
QML_ANONYMOUSqreal containerElevation
Definition menu.h:30
Fluid::ShapeValue verticalContainerShape
Definition menu.h:45
qreal verticalGroupContentPadding
Definition menu.h:63
qreal verticalViewportMargin
Definition menu.h:79
qreal verticalItemHeight
Definition menu.h:61
qreal closedScale
Definition menu.h:77
qreal horizontalViewportMargin
Definition menu.h:78
qreal topPadding
Definition menu.h:32
qreal verticalGroupPadding
Definition menu.h:62