From 59a42395b2143e768d9283e6f28155d3c883d903 Mon Sep 17 00:00:00 2001 From: Peter Hartmann Date: Mon, 7 Oct 2024 14:46:55 +0200 Subject: [PATCH] text input: Add M3 outlined mode --- designsystems/material3/QskMaterial3Skin.cpp | 43 +++++++-- designsystems/material3/QskMaterial3Skin.h | 1 + examples/gallery/inputs/InputPage.cpp | 98 ++++++++++++-------- src/controls/QskTextInput.cpp | 42 +++++++++ src/controls/QskTextInput.h | 17 ++++ src/controls/QskTextInputSkinlet.cpp | 84 ++++++++++++++++- 6 files changed, 231 insertions(+), 54 deletions(-) diff --git a/designsystems/material3/QskMaterial3Skin.cpp b/designsystems/material3/QskMaterial3Skin.cpp index 076850b6..81ca3140 100644 --- a/designsystems/material3/QskMaterial3Skin.cpp +++ b/designsystems/material3/QskMaterial3Skin.cpp @@ -451,12 +451,16 @@ void Editor::setupTextLabel() void Editor::setupTextInput() { using Q = QskTextInput; + using M3 = QskMaterial3Skin; const QskStateCombination allStates( QskStateCombination::CombinationNoState, QskAspect::AllStates ); // Panel setStrutSize( Q::Panel, -1.0, 56_dp ); + + // Panel - Filled + setGradient( Q::Panel, m_pal.surfaceVariant ); setBoxShape( Q::Panel, m_pal.shapeExtraSmallTop ); @@ -484,6 +488,16 @@ void Editor::setupTextInput() setGradient( Q::Panel | Q::Disabled, disabledPanelColor, allStates ); setBoxBorderColors( Q::Panel | Q::Disabled, m_pal.onSurface38, allStates ); + // Panel - Outlined + + setGradient( Q::Panel | M3::Outlined, Qt::transparent ); + setBoxShape( Q::Panel | M3::Outlined, m_pal.shapeExtraSmall ); + setBoxBorderMetrics( Q::Panel | M3::Outlined, 1_dp ); + setBoxBorderColors( Q::Panel | M3::Outlined, m_pal.outline ); + + setBoxBorderMetrics( Q::Panel | M3::Outlined | Q::Focused, 2_dp, allStates ); + setBoxBorderColors( Q::Panel | M3::Outlined | Q::Focused, m_pal.primary, allStates ); + // LeadingIcon @@ -492,18 +506,19 @@ void Editor::setupTextInput() const auto leadingIcon = symbol( "text_field_search" ); setSymbol( Q::LeadingIcon, leadingIcon ); - setGraphicRole( Q::LeadingIcon, QskMaterial3Skin::GraphicRoleOnSurface ); - setGraphicRole( Q::LeadingIcon | Q::Error, QskMaterial3Skin::GraphicRoleOnSurfaceVariant, allStates ); + setGraphicRole( Q::LeadingIcon, M3::GraphicRoleOnSurface ); + setGraphicRole( Q::LeadingIcon | Q::Error, M3::GraphicRoleOnSurfaceVariant, allStates ); // LabelText - setAlignment( Q::LabelText, Qt::AlignLeft | Qt::AlignVCenter ); + setAlignment( Q::LabelText, Qt::AlignLeft | Qt::AlignTop ); const QskStateCombination textEmptyStates( QskStateCombination::CombinationNoState, - Q::Focused | Q::Hovered | Q::ReadOnly | Q::Disabled ); + Q::Hovered | Q::ReadOnly | Q::Disabled | Q::Error ); - setFontRole( Q::LabelText | Q::TextEmpty, BodyMedium, textEmptyStates ); + setAlignment( Q::LabelText | Q::TextEmpty, Qt::AlignLeft | Qt::AlignVCenter, textEmptyStates ); + setFontRole( Q::LabelText | Q::TextEmpty, BodyLarge, textEmptyStates ); setColor( Q::LabelText | Q::TextEmpty, m_pal.onSurfaceVariant, textEmptyStates ); const QskStateCombination editingHoveredFocused( QskStateCombination::CombinationNoState, @@ -516,38 +531,47 @@ void Editor::setupTextInput() setColor( Q::LabelText | Q::Error, m_pal.error, allStates ); setColor( Q::LabelText | Q::Error | Q::Hovered, m_pal.onErrorContainer, allStates ); + // LabelText - Outlined + + setMargin( Q::LabelText | M3::Outlined, { 4_dp, 0, 4_dp, 0 }, editingHoveredFocused ); + // InputText setMargin( Q::InputText, { 16_dp, 8_dp, 16_dp, 8_dp } ); setColor( Q::InputText, m_pal.onSurface ); - setFontRole( Q::InputText, BodyMedium ); + setFontRole( Q::InputText, BodyLarge ); setAlignment( Q::InputText, Qt::AlignLeft | Qt::AlignBottom ); setColor( Q::InputText | Q::Error, m_pal.onSurface, allStates ); // same as with Hovered and Focused setColor( Q::InputText | Q::Disabled, m_pal.onSurface38 ); + // InputText - Outlined + + setAlignment( Q::InputText | M3::Outlined, Qt::AlignLeft | Qt::AlignVCenter ); + // HintText setColor( Q::HintText, color( Q::InputText ) ); setFontRole( Q::HintText, fontRole( Q::InputText ) ); setAlignment( Q::HintText, alignment( Q::InputText ) ); + setAlignment( Q::HintText | M3::Outlined, alignment( Q::InputText | M3::Outlined ) ); // TrailingIcon setStrutSize( Q::TrailingIcon, { 24_dp, 24_dp } ); setMargin( Q::TrailingIcon, { 0, 0, 12_dp, 0 } ); - setGraphicRole( Q::TrailingIcon, QskMaterial3Skin::GraphicRoleOnSurface ); + setGraphicRole( Q::TrailingIcon, M3::GraphicRoleOnSurfaceVariant ); const auto trailingIcon = symbol( "text_field_cancel" ); setSymbol( Q::TrailingIcon, trailingIcon ); const auto errorIcon = symbol( "text_field_error" ); setSymbol( Q::TrailingIcon | Q::Error, errorIcon, allStates ); - setGraphicRole( Q::TrailingIcon | Q::Error, QskMaterial3Skin::GraphicRoleError, allStates ); - setGraphicRole( Q::TrailingIcon | Q::Error | Q::Hovered, QskMaterial3Skin::GraphicRoleOnErrorContainer, allStates ); + setGraphicRole( Q::TrailingIcon | Q::Error, M3::GraphicRoleError, allStates ); + setGraphicRole( Q::TrailingIcon | Q::Error | Q::Hovered, M3::GraphicRoleOnErrorContainer, allStates ); // TrailingIconRipple @@ -1654,6 +1678,7 @@ QskMaterial3Theme::QskMaterial3Theme( QskSkin::ColorScheme colorScheme, elevation2 = QskShadowMetrics( -2, 8, { 0, 2 } ); elevation3 = QskShadowMetrics( -1, 11, { 0, 2 } ); + shapeExtraSmall = QskBoxShapeMetrics( 4_dp, 4_dp, 4_dp, 4_dp ); shapeExtraSmallTop = QskBoxShapeMetrics( 4_dp, 4_dp, 0, 0 ); } diff --git a/designsystems/material3/QskMaterial3Skin.h b/designsystems/material3/QskMaterial3Skin.h index f50d9b73..f6e7dcaf 100644 --- a/designsystems/material3/QskMaterial3Skin.h +++ b/designsystems/material3/QskMaterial3Skin.h @@ -102,6 +102,7 @@ class QSK_MATERIAL3_EXPORT QskMaterial3Theme qreal stateOpacity( int state ) const; + QskBoxShapeMetrics shapeExtraSmall; QskBoxShapeMetrics shapeExtraSmallTop; }; diff --git a/examples/gallery/inputs/InputPage.cpp b/examples/gallery/inputs/InputPage.cpp index 5b18b49a..d539433f 100644 --- a/examples/gallery/inputs/InputPage.cpp +++ b/examples/gallery/inputs/InputPage.cpp @@ -61,57 +61,70 @@ namespace } }; - class InputBox : public QskLinearBox + class TextInputBox : public QskLinearBox { public: - InputBox( QQuickItem* parent = nullptr ) + TextInputBox( QQuickItem* parent = nullptr ) : QskLinearBox( Qt::Horizontal, parent ) { - setSpacing( 40 ); + setSpacing( 25 ); setDefaultAlignment( Qt::AlignHCenter | Qt::AlignTop ); - { - auto spinBox = new QskSpinBox( 0.0, 100.0, 1.0, this ); - spinBox->setSizePolicy( Qt::Horizontal, QskSizePolicy::Fixed ); - spinBox->setPageSize( 5 ); - spinBox->setValue( 35 ); } + for( const auto& emphasis : { QskTextInput::NoEmphasis, QskTextInput::LowEmphasis } ) { - auto input = new QskTextInput( this ); - input->setLabelText( "filled" ); - input->setHintText( "hint text" ); - input->setSupportingText( "supporting text" ); - input->setMaxLength( 10 ); - } + { + auto input = new QskTextInput( this ); + input->setEmphasis( emphasis ); + const QString text = ( emphasis == QskTextInput::NoEmphasis ) ? "filled" : "outlined"; + input->setLabelText( text ); + input->setHintText( "hint text" ); + input->setSupportingText( "supporting text" ); + input->setMaxLength( 10 ); + } - { - auto input = new QskTextInput( this ); - input->setLeadingIcon( {} ); - input->setLabelText( "no leading icon" ); - input->setHintText( "hint text" ); - input->setSupportingText( "supporting text" ); - } - auto input = new QskTextInput( this ); - input->setSkinStateFlag( QskTextInput::Error ); - input->setLabelText( "error" ); - input->setHintText( "hint text" ); - input->setSupportingText( "error text" ); - } + { + auto input = new QskTextInput( this ); + input->setEmphasis( emphasis ); + input->setLeadingIcon( {} ); + input->setLabelText( "no leading icon" ); + input->setHintText( "hint text" ); + input->setSupportingText( "supporting text" ); + } + { + auto input = new QskTextInput( this ); + input->setEmphasis( emphasis ); + input->setLeadingIcon( {} ); + input->setLabelText( "no hint text" ); + } - { - auto input = new QskTextInput( this ); - input->setReadOnly( true ); - input->setLabelText( "read only" ); - input->setSizePolicy( Qt::Horizontal, QskSizePolicy::MinimumExpanding ); - } + { + auto input = new QskTextInput( this ); + input->setEmphasis( emphasis ); + input->setSkinStateFlag( QskTextInput::Error ); + input->setLabelText( "error" ); + input->setHintText( "hint text" ); + input->setSupportingText( "error text" ); + } - { - auto input = new QskTextInput( this ); - input->setMaxLength( 15 ); - input->setLabelText( "password" ); - input->setEchoMode( QskTextInput::Password ); - input->setHintText( "better be strong" ); + { + auto input = new QskTextInput( this ); + input->setEmphasis( emphasis ); + input->setReadOnly( true ); + input->setLabelText( "read only" ); + input->setSizePolicy( Qt::Horizontal, QskSizePolicy::MinimumExpanding ); + } + + { + auto input = new QskTextInput( this ); + input->setEmphasis( emphasis ); + input->setMaxLength( 15 ); + input->setLabelText( "password" ); + input->setEchoMode( QskTextInput::Password ); + input->setHintText( "better be strong" ); + } + } } }; } @@ -136,12 +149,17 @@ InputPage::InputPage( QQuickItem* parent ) auto spinBox = new QskSpinBox( 0.0, 100.0, 1.0 ); spinBox->setSizePolicy( Qt::Horizontal, QskSizePolicy::Fixed ); - auto inputBox = new InputBox(); + auto textInputBox = new TextInputBox(); inputBox->setSizePolicy( Qt::Vertical, QskSizePolicy::Fixed ); auto vBox = new QskLinearBox( Qt::Vertical ); vBox->setSpacing( 30 ); vBox->setExtraSpacingAt( Qt::RightEdge | Qt::BottomEdge ); + + auto spinBox = new QskSpinBox( 0.0, 100.0, 1.0 ); + spinBox->setSizePolicy( Qt::Horizontal, QskSizePolicy::Fixed ); + spinBox->setPageSize( 5 ); + spinBox->setValue( 35 ); vBox->addItem( sliders[0].continous ); vBox->addItem( sliders[0].discrete ); diff --git a/src/controls/QskTextInput.cpp b/src/controls/QskTextInput.cpp index 3f33fdbc..da0e72ad 100644 --- a/src/controls/QskTextInput.cpp +++ b/src/controls/QskTextInput.cpp @@ -293,6 +293,11 @@ namespace class QskTextInput::PrivateData { public: + PrivateData() + : emphasis( NoEmphasis ) + { + } + TextInput* textInput; QString labelText; QString hintText; @@ -300,6 +305,7 @@ class QskTextInput::PrivateData unsigned int activationModes : 3; bool hasPanel : 1; + int emphasis : 4; }; QskTextInput::QskTextInput( QQuickItem* parent ) @@ -563,6 +569,12 @@ QSizeF QskTextInput::layoutSizeHint( Qt::SizeHint which, const QSizeF& ) const hint = hint.expandedTo( strutSizeHint( Panel ) ); } + if( emphasis() == LowEmphasis ) + { + const auto fontHeight = effectiveFontHeight( LabelText | TextEmpty ); + hint.rheight() += fontHeight / 2; + } + if( !supportingText().isEmpty() || maxLength() != 32767 ) // magic number hardcoded in qquicktextinput.cpp { const auto margins = marginHint( SupportingText ); @@ -587,6 +599,24 @@ void QskTextInput::updateNode( QSGNode* node ) Inherited::updateNode( node ); } +void QskTextInput::setEmphasis( Emphasis emphasis ) +{ + if ( emphasis != m_data->emphasis ) + { + m_data->emphasis = emphasis; + + resetImplicitSize(); + update(); + + Q_EMIT emphasisChanged( emphasis ); + } +} + +QskTextInput::Emphasis QskTextInput::emphasis() const +{ + return static_cast< Emphasis >( m_data->emphasis ); +} + QString QskTextInput::inputText() const { return m_data->textInput->text(); @@ -798,6 +828,18 @@ void QskTextInput::ensureVisible( int position ) m_data->textInput->ensureVisible( position ); } +QskAspect::Variation QskTextInput::effectiveVariation() const +{ + switch( m_data->emphasis ) + { + case LowEmphasis: + return QskAspect::Small; + + default: + return QskAspect::NoVariation; + } +} + int QskTextInput::cursorPosition() const { return m_data->textInput->cursorPosition(); diff --git a/src/controls/QskTextInput.h b/src/controls/QskTextInput.h index f4ab77f4..d03516bb 100644 --- a/src/controls/QskTextInput.h +++ b/src/controls/QskTextInput.h @@ -58,6 +58,9 @@ class QSK_EXPORT QskTextInput : public QskControl Q_PROPERTY( bool panel READ hasPanel WRITE setPanel NOTIFY panelChanged ) + Q_PROPERTY( Emphasis emphasis READ emphasis + WRITE setEmphasis NOTIFY emphasisChanged ) + using Inherited = QskControl; public: @@ -67,6 +70,13 @@ class QSK_EXPORT QskTextInput : public QskControl QSK_STATES( ReadOnly, Editing, Selected, Error, TextEmpty ) + enum Emphasis + { + LowEmphasis = -1, + NoEmphasis = 0, + }; + Q_ENUM( Emphasis ) + enum ActivationMode { NoActivation, @@ -99,6 +109,9 @@ class QSK_EXPORT QskTextInput : public QskControl void setupFrom( const QQuickItem* ); + void setEmphasis( Emphasis ); + Emphasis emphasis() const; + QString inputText() const; QString labelText() const; @@ -179,6 +192,8 @@ class QSK_EXPORT QskTextInput : public QskControl void ensureVisible( int position ); + QskAspect::Variation effectiveVariation() const override; + public Q_SLOTS: void setInputText( const QString& ); void setLabelText( const QString& ); @@ -186,6 +201,8 @@ class QSK_EXPORT QskTextInput : public QskControl void setEditing( bool ); Q_SIGNALS: + void emphasisChanged( Emphasis ); + void editingChanged( bool ); void activationModesChanged(); diff --git a/src/controls/QskTextInputSkinlet.cpp b/src/controls/QskTextInputSkinlet.cpp index 40756b58..902c2b1b 100644 --- a/src/controls/QskTextInputSkinlet.cpp +++ b/src/controls/QskTextInputSkinlet.cpp @@ -6,6 +6,9 @@ #include "QskTextInputSkinlet.h" #include "QskTextInput.h" +#include "QskBoxBorderColors.h" +#include "QskBoxBorderMetrics.h" +#include "QskBoxShapeMetrics.h" #include "QskFunctions.h" #include @@ -20,6 +23,39 @@ namespace + " / " + QString::number( input->maxLength() ); return s; } + + // We need to "cut a hole" in the upper gradient for the label text: + QskBoxBorderColors outlineColors( const QskTextInput* input ) + { + auto borderColors = input->boxBorderColorsHint( Q::Panel ); + auto topGradient = borderColors.gradientAt( Qt::TopEdge ); + + const auto panelRect = input->subControlRect( Q::Panel ); + + const auto margins = input->marginHint( Q::LabelText ); + const auto iconMargins = input->marginHint( Q::LeadingIcon ); + + const auto x1 = iconMargins.left() - margins.left(); + const auto r1 = x1 / panelRect.width(); + + const auto w = qskHorizontalAdvance( input->effectiveFont( Q::LabelText ), input->labelText() ); + + const auto x2 = x1 + w + margins.right(); + const auto r2 = x2 / panelRect.width(); + + topGradient.setStops( { + { 0.0, topGradient.startColor() }, + { r1, topGradient.startColor() }, + { r1, Qt::transparent }, + { r2, Qt::transparent }, + { r2, topGradient.startColor() }, + { 1.0, topGradient.startColor() } + } ); + + borderColors.setGradientAt( Qt::TopEdge, topGradient ); + + return borderColors; + } } QskTextInputSkinlet::QskTextInputSkinlet( QskSkin* skin ) @@ -50,6 +86,13 @@ QRectF QskTextInputSkinlet::subControlRect( const QskSkinnable* skinnable, { auto rect = contentsRect; + if( input->emphasis() == Q::LowEmphasis ) + { + const auto fontHeight = input->effectiveFontHeight( Q::LabelText | Q::Focused ); + rect.setY( fontHeight / 2 ); + } + + const auto h = input->strutSizeHint( subControl ).height(); rect.setHeight( h ); @@ -78,13 +121,27 @@ QRectF QskTextInputSkinlet::subControlRect( const QskSkinnable* skinnable, { const auto inputRect = input->subControlRect( Q::InputText ); - if( input->hasSkinState( Q::Focused ) || !input->inputText().isEmpty() ) + if( !input->inputText().isEmpty() + || input->hasSkinState( Q::Focused ) + || input->hasSkinState( Q::Editing ) ) { const auto margins = input->marginHint( subControl ); auto rect = inputRect; - rect.setY( contentsRect.y() + margins.top() ); - const QFontMetricsF fm ( input->effectiveFont( subControl ) ); + const QFontMetricsF fm( input->effectiveFont( subControl ) ); + + if( input->emphasis() == Q::LowEmphasis ) + { + const auto iconMargins = input->marginHint( Q::LeadingIcon ); + rect.setX( iconMargins.left() ); + rect.setY( 0 ); + } + else + { + rect.setY( contentsRect.y() + margins.top() ); + } + rect.setHeight( fm.height() ); + return rect; } else @@ -107,7 +164,8 @@ QRectF QskTextInputSkinlet::subControlRect( const QskSkinnable* skinnable, } else if ( subControl == Q::HintText ) { - if( input->hasSkinState( Q::Focused ) && input->inputText().isEmpty() ) // ### has TextEmpty state + if( input->hasSkinState( Q::TextEmpty ) + && ( input->hasSkinState( Q::Focused ) || input->hasSkinState( Q::Editing ) ) ) { return input->subControlRect( Q::InputText ); } @@ -213,7 +271,23 @@ QSGNode* QskTextInputSkinlet::updateSubNode( if ( !input->hasPanel() ) return nullptr; - return updateBoxNode( skinnable, node, Q::Panel ); + if( input->emphasis() == Q::LowEmphasis + && ( !input->hasSkinState( Q::TextEmpty ) + || input->hasSkinState( Q::Focused ) + || input->hasSkinState( Q::Editing ) ) ) + { + const auto shape = skinnable->boxShapeHint( Q::Panel ); + const auto borderMetrics = skinnable->boxBorderMetricsHint( Q::Panel ); + const auto borderColors = outlineColors( input ); + const auto gradient = input->gradientHint( Q::Panel ); + + return updateBoxNode( skinnable, node, input->subControlRect( Q::Panel ), + shape, borderMetrics, borderColors, gradient ); + } + else + { + return updateBoxNode( skinnable, node, Q::Panel ); + } } case LeadingIconRole: