Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qquick3drenderpass.cpp
Go to the documentation of this file.
1// Copyright (C) 2025 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3// Qt-Security score:significant reason:default
4
5
7
8#include <QtQuick3DRuntimeRender/private/qssgrenderuserpass_p.h>
9#include <QtQuick3DRuntimeRender/private/qssgshadermaterialadapter_p.h>
10
11#include <QtCore/QLoggingCategory>
12
14
15Q_LOGGING_CATEGORY(lcQuick3DRenderPass, "qt.quick3d.renderpass")
16
17/*!
18 \qmltype RenderPass
19 \inherits Object3D
20 \inqmlmodule QtQuick3D
21 \brief Defines a custom render pass for rendering 3D content.
22 \since 6.11
23
24 A RenderPass defines a rendering step and the render target it writes
25 into. It is the combination of three concerns:
26
27 \list
28 \li \b {Where to render} — one or more output textures declared as
29 \l ColorAttachment or \l DepthTextureAttachment commands in
30 \l {RenderPass::commands}{commands}.
31 \li \b {What to render} — which scene objects the pass draws,
32 controlled by \l RenderablesFilter commands. Sub-divisions of
33 work within the same render target are described using
34 \l SubRenderPass commands.
35 \li \b {How to render} — the \l materialMode and any
36 \l PipelineStateOverride commands.
37 \endlist
38
39 A RenderPass becomes active for a scene when it is placed as a child
40 of a \l View3D or a \l Node.
41
42 The following example sets up a simple off-screen pass that renders
43 all scene objects into a custom texture, which can then be consumed
44 by a material or a \l SimpleQuadRenderer:
45
46 \qml
47 import QtQuick3D
48
49 View3D {
50 // Declare the off-screen color buffer
51 RenderPassTexture {
52 id: myColorTexture
53 format: RenderPassTexture.RGBA8
54 }
55
56 // The render pass: where + what + how
57 RenderPass {
58 id: myRenderPass
59 commands: [
60 // Where: attach the texture as the color output
61 ColorAttachment {
62 name: "color0"
63 target: myColorTexture
64 },
65 // What: render all objects into it
66 RenderablesFilter {
67 renderableTypes: RenderablesFilter.Opaque | RenderablesFilter.Transparent
68 }
69 ]
70 }
71 }
72 \endqml
73
74 \section1 Exposing data to the shaders
75
76 As with Effects and Custom Materials, the RenderPass will expose and
77 update user-defined properties to the shaders automatically. Any QML
78 properties declared on a RenderPass subtype will be available as
79 uniforms in the shader.
80
81 \sa SubRenderPass, RenderOutputProvider, RenderablesFilter
82*/
83
84QQuick3DRenderPass::QQuick3DRenderPass(QQuick3DObject *parent)
85 : QQuick3DObject(*(new QQuick3DObjectPrivate(QQuick3DObjectPrivate::Type::RenderPass, QQuick3DObjectPrivate::Flags::RequiresSecondaryUpdate)), parent)
86 , QQuick3DPropertyChangedTracker(this, QQuick3DSuperClassInfo<QQuick3DRenderPass>())
87{
88}
89
90QSSGRenderGraphObject *QQuick3DRenderPass::updateSpatialNode(QSSGRenderGraphObject *node)
91{
92 QSSGRenderUserPass *renderPassNode = static_cast<QSSGRenderUserPass *>(node);
93
94 bool newBackendNode = false;
95 if (!renderPassNode) {
96 renderPassNode = new QSSGRenderUserPass;
97 newBackendNode = true;
98 }
99
100 const bool fullUpdate = newBackendNode || (m_dirtyAttributes & Dirty::TextureDirty) || (m_dirtyAttributes & CommandsDirty);
101
102 auto &shaderAugmentation = renderPassNode->shaderAugmentation;
103 auto &uniformProps = shaderAugmentation.propertyUniforms;
104
105 if (fullUpdate) {
106 markAllDirty();
107
108 // Properties -> uniforms.
109 uniformProps = extractProperties();
110
111 // Commands
112 renderPassNode->resetCommands();
113 clearDirty(Dirty::CommandsDirty);
114 for (QQuick3DShaderUtilsRenderCommand *command : std::as_const(m_commands)) {
115 if (auto *cmd = command->cloneCommand())
116 renderPassNode->commands.push_back(cmd);
117 else
118 markDirty(CommandsDirty, true); // Try again next time
119 }
120 }
121
122 // Update the property values
123 if (m_dirtyAttributes & Dirty::PropertyDirty) {
124 for (const auto &prop : std::as_const(uniformProps)) {
125 auto p = metaObject()->property(prop.pid);
126 if (Q_LIKELY(p.isValid())) {
127 QVariant v = p.read(this);
128 if (v.isValid()) {
129 if (v.metaType().id() == qMetaTypeId<QQuick3DTexture *>()) {
130 QQuick3DTexture *tex = v.value<QQuick3DTexture *>();
131 auto *po = QQuick3DObjectPrivate::get(tex);
132 QSSGRenderImage *ri = static_cast<QSSGRenderImage *>(po->spatialNode);
133 prop.value = QVariant::fromValue(ri);
134 } else {
135 prop.value = v;
136 }
137 }
138 }
139 }
140
141 clearDirty(Dirty(Dirty::PropertyDirty | Dirty::TextureDirty));
142 }
143
144 // Clear Dirty
145 if (m_dirtyAttributes & Dirty::ClearDirty) {
146 renderPassNode->renderTargetFlags = QRhiTextureRenderTarget::Flags(m_renderTargetFlags.toInt());
147 renderPassNode->clearColor = m_clearColor;
148 renderPassNode->depthStencilClearValue = { m_depthClearValue, m_stencilClearValue };
149
150 clearDirty(Dirty::ClearDirty);
151 }
152
153 if (m_dirtyAttributes & Dirty::PassTypeDirty) {
154 switch (m_passMode) {
155 case UserPass:
156 renderPassNode->passMode = QSSGRenderUserPass::UserPass;
157 break;
158 case SkyboxPass:
159 renderPassNode->passMode = QSSGRenderUserPass::SkyboxPass;
160 break;
161 case Item2DPass:
162 renderPassNode->passMode = QSSGRenderUserPass::Item2DPass;
163 break;
164 }
165
166 clearDirty(Dirty::PassTypeDirty);
167 }
168
169 // If not a user pass, we're done
170 if (m_passMode != UserPass)
171 return renderPassNode;
172
173 renderPassNode->materialMode = QSSGRenderUserPass::MaterialModes(m_materialMode);
174 clearDirty(Dirty::MaterialModeDirty);
175
176 if (renderPassNode->materialMode == QSSGRenderUserPass::OverrideMaterial) {
177 clearDirty(Dirty::OverrideMaterialDirty);
178 if (m_overrideMaterial) {
179 // Set the backend material
180 QSSGRenderGraphObject *graphObject = QQuick3DObjectPrivate::get(m_overrideMaterial)->spatialNode;
181 if (graphObject)
182 renderPassNode->overrideMaterial = graphObject;
183 else
184 markDirty(OverrideMaterialDirty, true); // Try again next time
185 } else {
186 // Set nullptr
187 renderPassNode->overrideMaterial = nullptr;
188 }
189 } else if (renderPassNode->materialMode == QSSGRenderUserPass::OriginalMaterial) {
190 // Nothing to do
191 } else if (renderPassNode->materialMode == QSSGRenderUserPass::AugmentMaterial) {
192 // Augment Shaders
193 if (!m_augmentShader.isEmpty()) {
194 const QQmlContext *context = qmlContext(this);
195 QByteArray shaderPathKey("augment material --");
196 QByteArray augment = QSSGShaderUtils::resolveShader(m_augmentShader, context, shaderPathKey);
197 QByteArray augmentSnippet;
198 QByteArray augmentPreamble;
199
200 // We have to pick apart the shader string such that the contents of the:
201 // void MAIN_FRAGMENT_AUGMENT() { }
202 // function are taken out, and will get added to the end of the shader generation
203 // and the goal is to overwrite the "output" of the shader
204
205 // We also need to scan the who shader code for certain "keywords" so that we know
206 // what features to enable in the original material.
207
208 // Everything else outsode of MAIN_FRAGMENT_AUGMENT function ends up being preamble code
209 // that will get pasted in before the real main(). So that will include helper functions and
210 // resolvable #includes etc.
211
212 static const char *mainFuncStart = "void MAIN_FRAGMENT_AUGMENT()";
213 qsizetype mainFuncIdx = augment.indexOf(mainFuncStart);
214 if (mainFuncIdx != -1) {
215 qsizetype braceOpenIdx = augment.indexOf('{', mainFuncIdx + int(strlen(mainFuncStart)));
216 if (braceOpenIdx != -1) {
217 qsizetype braceCloseIdx = braceOpenIdx;
218 qsizetype openBraces = 1;
219 while (openBraces > 0 && braceCloseIdx + 1 < augment.size()) {
220 braceCloseIdx++;
221 if (augment[braceCloseIdx] == '{')
222 openBraces++;
223 else if (augment[braceCloseIdx] == '}')
224 openBraces--;
225 }
226 if (openBraces == 0) {
227 // We found the closing brace
228 augmentSnippet = augment.mid(braceOpenIdx + 1, braceCloseIdx - braceOpenIdx - 1);
229 augmentPreamble = augment.left(mainFuncIdx);
230 augmentPreamble += augment.mid(braceCloseIdx + 1);
231 } else {
232 qWarning("QQuick3DRenderPass: Could not find the closing brace of MAIN_FRAGMENT_AUGMENT() in shader %s", qPrintable(m_augmentShader.toString()));
233 }
234 } else {
235 qWarning("QQuick3DRenderPass: Could not find the opening brace of MAIN_FRAGMENT_AUGMENT() in shader %s", qPrintable(m_augmentShader.toString()));
236 }
237 } else {
238 qWarning("QQuick3DRenderPass: Could not find MAIN_FRAGMENT_AUGMENT() function in shader %s", qPrintable(m_augmentShader.toString()));
239 }
240
241 renderPassNode->shaderAugmentation.body = augmentSnippet;
242 renderPassNode->shaderAugmentation.preamble = augmentPreamble;
243 renderPassNode->markDirty(QSSGRenderUserPass::DirtyFlag::ShaderDirty);
244 }
245 }
246
247 return renderPassNode;
248}
249
250void QQuick3DRenderPass::itemChange(ItemChange change, const ItemChangeData &value)
251{
252 if (change == QQuick3DObject::ItemSceneChange)
253 updateSceneManager(value.sceneManager);
254}
255
256void QQuick3DRenderPass::markTrackedPropertyDirty(QMetaProperty property, DirtyPropertyHint hint)
257{
258 Q_UNUSED(property);
259
260 // FIXME: As with the property tracking for Effects and Custom materials we
261 // should really track which property changed and only update that one.
262 if (hint == DirtyPropertyHint::Reference) {
263 // FIXME: We should verify that the property is actually a texture property.
264 markDirty(Dirty::TextureDirty);
265 } else {
266 markDirty(Dirty::PropertyDirty);
267 }
268}
269
270void QQuick3DRenderPass::onMaterialDestroyed(QObject *object)
271{
272 if (m_overrideMaterial == object) {
273 m_overrideMaterial = nullptr;
274 emit overrideMaterialChanged();
275 markDirty(OverrideMaterialDirty);
276 }
277}
278
279void QQuick3DRenderPass::onCommandChanged()
280{
281 markDirty(CommandsDirty);
282}
283
284void QQuick3DRenderPass::qmlAppendCommand(QQmlListProperty<QQuick3DShaderUtilsRenderCommand> *list, QQuick3DShaderUtilsRenderCommand *command)
285{
286 if (!command)
287 return;
288
289 QQuick3DRenderPass *that = qobject_cast<QQuick3DRenderPass *>(list->object);
290
291 if (!command->parentItem())
292 command->setParentItem(that);
293
294 that->m_commands.push_back(command);
295 that->markDirty(CommandsDirty);
296
297 // Re-clone commands whenever a property changes so the render thread
298 // sees the updated state.
299 QObject::connect(command, &QQuick3DShaderUtilsRenderCommand::changed, that, &QQuick3DRenderPass::onCommandChanged);
300}
301
302QQuick3DShaderUtilsRenderCommand *QQuick3DRenderPass::qmlCommandAt(QQmlListProperty<QQuick3DShaderUtilsRenderCommand> *list, qsizetype index)
303{
304 QQuick3DRenderPass *that = qobject_cast<QQuick3DRenderPass *>(list->object);
305 return that->m_commands.at(index);
306}
307
308qsizetype QQuick3DRenderPass::qmlCommandCount(QQmlListProperty<QQuick3DShaderUtilsRenderCommand> *list)
309{
310 QQuick3DRenderPass *that = qobject_cast<QQuick3DRenderPass *>(list->object);
311 return that->m_commands.size();
312}
313
314void QQuick3DRenderPass::qmlCommandClear(QQmlListProperty<QQuick3DShaderUtilsRenderCommand> *list)
315{
316 QQuick3DRenderPass *that = qobject_cast<QQuick3DRenderPass *>(list->object);
317 for (QQuick3DShaderUtilsRenderCommand *cmd : std::as_const(that->m_commands))
318 QObject::disconnect(cmd, nullptr, that, nullptr);
319 that->m_commands.clear();
320 that->markDirty(CommandsDirty);
321}
322
323void QQuick3DRenderPass::updateSceneManager(QQuick3DSceneManager *sceneManager)
324{
325 if (sceneManager) {
326 // Handle inline override material that may not have had a scene manager when it was set
327 if (m_overrideMaterial && !m_overrideMaterial->parentItem() && !QQuick3DObjectPrivate::get(m_overrideMaterial)->sceneManager) {
328 if (!m_overrideMaterialRefed) {
329 QQuick3DObjectPrivate::refSceneManager(m_overrideMaterial, *sceneManager);
330 m_overrideMaterialRefed = true;
331 }
332 }
333 } else {
334 // Deref the material when scene manager is removed
335 if (m_overrideMaterial && m_overrideMaterialRefed) {
336 QQuick3DObjectPrivate::derefSceneManager(m_overrideMaterial);
337 m_overrideMaterialRefed = false;
338 }
339 }
340}
341
342void QQuick3DRenderPass::markDirty(Dirty type, bool requestSecondaryUpdate)
343{
344 if (!(m_dirtyAttributes & quint32(type))) {
345 m_dirtyAttributes |= quint32(type);
346 update();
347 }
348
349 if (requestSecondaryUpdate)
350 QQuick3DObjectPrivate::get(this)->requestSecondaryUpdate();
351}
352
353void QQuick3DRenderPass::clearDirty(Dirty type)
354{
355 m_dirtyAttributes &= ~quint32(type);
356}
357
358/*!
359 \qmlproperty list<RenderCommand> RenderPass::commands
360 This property holds the list of render commands for the render pass.
361
362 The commands in the list are executed in the order they appear in the list.
363
364 \note The commands for RenderPass and Effects are similar but not the same, only
365 those marked as compatible can be used with this RenderPass.
366
367 \sa SubRenderPass,
368 PipelineStateOverride,
369 RenderablesFilter,
370 RenderPassTexture,
371 ColorAttachment,
372 DepthTextureAttachment,
373 DepthStencilAttachment,
374 AddDefine,
375 renderTargetBlend
376*/
377
378QQmlListProperty<QQuick3DShaderUtilsRenderCommand> QQuick3DRenderPass::commands()
379{
380 return QQmlListProperty<QQuick3DShaderUtilsRenderCommand>(this,
381 nullptr,
382 QQuick3DRenderPass::qmlAppendCommand,
383 QQuick3DRenderPass::qmlCommandCount,
384 QQuick3DRenderPass::qmlCommandAt,
385 QQuick3DRenderPass::qmlCommandClear);
386}
387
388/*!
389 \qmlproperty color RenderPass::clearColor
390 This property holds the clear color for the render pass.
391
392 \default Qt.black
393*/
394QColor QQuick3DRenderPass::clearColor() const
395{
396 return m_clearColor;
397}
398
399void QQuick3DRenderPass::setClearColor(const QColor &newClearColor)
400{
401 if (m_clearColor == newClearColor)
402 return;
403 m_clearColor = newClearColor;
404 emit clearColorChanged();
405 markDirty(ClearDirty);
406}
407
408/*!
409 \qmlproperty RenderPass::MaterialModes RenderPass::materialMode
410 Controls how object materials are handled when rendering into this pass.
411
412 \value RenderPass.OriginalMaterial
413 Objects are rendered using their own assigned materials, with full
414 lighting, textures, and material properties applied normally. This is
415 the standard mode for rendering a faithful copy of the scene into a
416 custom render target — for example, a secondary viewpoint for a
417 reflection probe, a rear-view camera, or a picture-in-picture effect.
418 The \c overrideMaterial, \c augmentShader, and \c shaders properties
419 are not used in this mode.
420
421 \value RenderPass.AugmentMaterial
422 Each object is rendered with its own material, but the contents of
423 the \c {MAIN_FRAGMENT_AUGMENT()} function defined in \l augmentShader
424 are injected after the original material's output definition. This
425 allows the augment code to read the material's computed color and write
426 to additional color outputs defined by \l ColorAttachment commands in
427 the pass. This is useful for multi-render-target (MRT) passes that need
428 per-material shading, such as writing the lit color to one attachment
429 and a world-space normal to another in a single draw call.
430
431 \value RenderPass.OverrideMaterial
432 All objects rendered by this pass use the single \l overrideMaterial
433 instead of their own. This is useful for depth-only passes, shadow maps,
434 silhouette or outline effects, and any other case where you want all
435 geometry to be shaded identically regardless of what material is
436 assigned to it. The \c augmentShader property is not used in this mode.
437
438 \default RenderPass.OriginalMaterial
439*/
440QQuick3DRenderPass::MaterialModes QQuick3DRenderPass::materialMode() const
441{
442 return m_materialMode;
443}
444
445void QQuick3DRenderPass::setMaterialMode(MaterialModes newMaterialMode)
446{
447 if (m_materialMode == newMaterialMode)
448 return;
449 m_materialMode = newMaterialMode;
450 emit materialModeChanged();
451 markDirty(MaterialModeDirty);
452}
453
454/*!
455 \qmlproperty Material RenderPass::overrideMaterial
456 This property holds the override material for the render pass when
457 \l{RenderPass::materialMode}{materialMode} is set to \c OverrideMaterial.
458*/
459QQuick3DMaterial *QQuick3DRenderPass::overrideMaterial() const
460{
461 return m_overrideMaterial;
462}
463
464void QQuick3DRenderPass::setOverrideMaterial(QQuick3DMaterial *newOverrideMaterial)
465{
466 if (m_overrideMaterial == newOverrideMaterial)
467 return;
468
469 // Deref the old material if we had ref'd it
470 if (m_overrideMaterial && m_overrideMaterialRefed) {
471 QQuick3DObjectPrivate::derefSceneManager(m_overrideMaterial);
472 m_overrideMaterialRefed = false;
473 }
474
475 QQuick3DObjectPrivate::attachWatcher(this, &QQuick3DRenderPass::setOverrideMaterial, newOverrideMaterial, m_overrideMaterial);
476
477 m_overrideMaterial = newOverrideMaterial;
478
479 // Handle inline material declarations by ensuring they get registered with the scene manager
480 if (m_overrideMaterial && m_overrideMaterial->parentItem() == nullptr) {
481 // If the material has no parent, check if it has a hierarchical parent that's a QQuick3DObject
482 // and re-parent it to that, e.g., inline materials
483 QQuick3DObject *parentItem = qobject_cast<QQuick3DObject *>(m_overrideMaterial->parent());
484 if (parentItem) {
485 m_overrideMaterial->setParentItem(parentItem);
486 } else {
487 // If no valid parent was found, make sure the material refs our scene manager
488 const auto &sceneManager = QQuick3DObjectPrivate::get(this)->sceneManager;
489 if (sceneManager) {
490 QQuick3DObjectPrivate::refSceneManager(m_overrideMaterial, *sceneManager);
491 m_overrideMaterialRefed = true;
492 }
493 // else: If there's no scene manager, defer until one is set, see itemChange()
494 }
495 }
496
497 emit overrideMaterialChanged();
498 markDirty(OverrideMaterialDirty);
499}
500
501/*!
502 \qmlproperty url RenderPass::augmentShader
503 This property holds the augment shader URL for the render pass when
504 \l{RenderPass::materialMode}{materialMode} is set to \c AugmentMaterial.
505
506 The shader file should contain a function with the following signature:
507 \badcode
508 void MAIN_FRAGMENT_AUGMENT() {
509 // Custom shader code here
510 }
511 \endcode
512
513 This function will be combined with the existing fragment shader of the material
514 being used by the object being rendered in this render pass. Allowing users to
515 augment the existing material shader with custom code.
516*/
517QUrl QQuick3DRenderPass::augmentShader() const
518{
519 return m_augmentShader;
520}
521
522void QQuick3DRenderPass::setAugmentShader(const QUrl &newAugmentShader)
523{
524 if (m_augmentShader == newAugmentShader)
525 return;
526 m_augmentShader = newAugmentShader;
527 emit augmentShaderChanged();
528 markDirty(AugmentShaderDirty);
529}
530
531/*!
532 \qmlproperty RenderPass::PassMode RenderPass::passMode
533 This property holds the pass mode for the render pass.
534
535 In addition to standard user render passes, Qt Quick 3D supports
536 users to manually triggering internal render passes for rendering
537 the skybox and 2D items.
538
539 \value RenderPass.UserPass A user specified render pass.
540 \value RenderPass.SkyboxPass Qt Quick 3D's built-in skybox render pass.
541 \value RenderPass.Item2DPass Qt Quick 3D's built-in 2D item render pass.
542 \default RenderPass.UserPass
543*/
544
545QQuick3DRenderPass::PassMode QQuick3DRenderPass::passMode() const
546{
547 return m_passMode;
548}
549
550void QQuick3DRenderPass::setPassMode(PassMode newPassMode)
551{
552 if (m_passMode == newPassMode)
553 return;
554 m_passMode = newPassMode;
555 emit passModeChanged();
556 markDirty(PassTypeDirty);
557}
558
559/*!
560 \qmlproperty real RenderPass::depthClearValue
561 This property holds the depth clear value for the render pass.
562
563 \default 1.0
564*/
565float QQuick3DRenderPass::depthClearValue() const
566{
567 return m_depthClearValue;
568}
569
570void QQuick3DRenderPass::setDepthClearValue(float newDepthClearValue)
571{
572 if (qFuzzyCompare(m_depthClearValue, newDepthClearValue))
573 return;
574 m_depthClearValue = newDepthClearValue;
575 emit depthClearValueChanged();
576 markDirty(ClearDirty);
577}
578
579/*!
580 \qmlproperty int RenderPass::stencilClearValue
581 This property holds the stencil clear value for the render pass.
582
583 \default 0
584*/
585quint32 QQuick3DRenderPass::stencilClearValue() const
586{
587 return m_stencilClearValue;
588}
589
590void QQuick3DRenderPass::setStencilClearValue(quint32 newStencilClearValue)
591{
592 if (m_stencilClearValue == newStencilClearValue)
593 return;
594 m_stencilClearValue = newStencilClearValue;
595 emit stencilClearValueChanged();
596 markDirty(ClearDirty);
597}
598
599/*!
600 \qmlproperty RenderPass::RenderTargetFlags RenderPass::renderTargetFlags
601 This property holds the render target flags for the render pass. These flags affect how
602 the render target contents are handled at the beginning and end of each frame.
603
604 \value RenderPass.None No special behavior. Color and depth/stencil contents are cleared at the start of each frame.
605 \value RenderPass.PreserveColorContents Preserve the color contents of the render target between frames, so the previous frame's output remains until explicitly overwritten.
606 \value RenderPass.PreserveDepthStencilContents Preserve the depth and stencil contents of the render target between frames.
607 \value RenderPass.DoNotStoreDepthStencilContents Do not store the depth and stencil contents of the render target after rendering (may improve performance on tiled GPUs).
608
609 \default RenderPass.None
610
611 \sa QRhiTextureRenderTarget::Flags
612*/
613
614QQuick3DRenderPass::RenderTargetFlags QQuick3DRenderPass::renderTargetFlags() const
615{
616 return m_renderTargetFlags;
617}
618
619void QQuick3DRenderPass::setRenderTargetFlags(RenderTargetFlags newRenderTargetFlags)
620{
621 if (m_renderTargetFlags == newRenderTargetFlags)
622 return;
623 m_renderTargetFlags = newRenderTargetFlags;
624 emit renderTargetFlagsChanged();
625 markDirty(ClearDirty);
626}
627
628QT_END_NAMESPACE
Combined button and popup list for selecting options.