diff --git a/designsystems/fluent2/QskFluent2Icons.qrc b/designsystems/fluent2/QskFluent2Icons.qrc index 7587fb4c..0d9b967d 100644 --- a/designsystems/fluent2/QskFluent2Icons.qrc +++ b/designsystems/fluent2/QskFluent2Icons.qrc @@ -3,5 +3,7 @@ icons/qvg/checkmark.qvg icons/qvg/chevron_down.qvg icons/qvg/chevron_up.qvg + icons/qvg/dismiss.qvg + icons/qvg/search.qvg diff --git a/designsystems/fluent2/QskFluent2Skin.cpp b/designsystems/fluent2/QskFluent2Skin.cpp index e38340ce..127bb3b9 100644 --- a/designsystems/fluent2/QskFluent2Skin.cpp +++ b/designsystems/fluent2/QskFluent2Skin.cpp @@ -1873,6 +1873,22 @@ void Editor::setupTextFieldMetrics() setAlignment( Q::Placeholder, Qt::AlignLeft | Qt::AlignVCenter ); setFontRole( Q::Placeholder, fontRole( Q::Text ) ); + + setStrutSize( Q::Header, { -1, 30_px } ); + setFontRole( Q::Header, Fluent2::Body ); + + setAlignment( Q::Text, Qt::AlignLeft | Qt::AlignVCenter ); + setFontRole( Q::Text, Fluent2::Body ); + + + setSymbol( Q::Icon, symbol( "search" ) ); + setSymbol( Q::Button, symbol( "dismiss" ) ); + + for ( const auto subControl : { Q::Icon, Q::Button } ) + { + setMargin( subControl, 2_px ); + setStrutSize( subControl, 16_px, 16_px ); + } } void Editor::setupTextFieldColors( @@ -1880,54 +1896,66 @@ void Editor::setupTextFieldColors( { using Q = QskTextField; using A = QskAspect; + using W = QskFluent2Skin; const auto& pal = theme.palette; - const auto text = Q::Text | section; - -#if 1 - setColor( text, pal.fillColor.text.primary ); - setColor( text | Q::Selected, pal.fillColor.textOnAccent.selectedText ); - setColor( text | Q::Disabled, pal.fillColor.text.disabled ); -#endif + setColor( Q::Text | section | Q::Selected, pal.fillColor.textOnAccent.selectedText ); + setColor( Q::TextPanel | section | Q::Selected, pal.fillColor.accent.selectedTextBackground ); - setColor( Q::TextPanel | Q::Selected, pal.fillColor.accent.selectedTextBackground ); - setColor( Q::Placeholder, pal.fillColor.text.secondary ); + setColor( Q::Placeholder | section, pal.fillColor.text.secondary ); + + setColor( Q::Header | section, pal.fillColor.text.primary ); for( const auto state : { A::NoState, Q::Hovered, Q::Focused, Q::Editing, Q::Disabled } ) { - QRgb panelColor, borderColor1, borderColor2; + QRgb panelColor, borderColor1, borderColor2, textColor; if ( state == Q::Hovered ) { panelColor = pal.fillColor.control.secondary; borderColor1 = pal.elevation.textControl.border[0]; borderColor2 = pal.elevation.textControl.border[1]; + textColor = pal.fillColor.text.primary; } else if ( ( state == Q::Focused ) || ( state == Q::Editing ) ) { panelColor = pal.fillColor.control.inputActive; borderColor1 = pal.elevation.textControl.border[0]; borderColor2 = pal.fillColor.accent.defaultColor; + textColor = pal.fillColor.text.primary; } else if ( state == Q::Disabled ) { panelColor = pal.fillColor.control.disabled; borderColor1 = borderColor2 = pal.strokeColor.control.defaultColor; + textColor = pal.fillColor.text.disabled; } else // A::NoState { panelColor = pal.fillColor.control.defaultColor; borderColor1 = pal.elevation.textControl.border[0]; borderColor2 = pal.elevation.textControl.border[1]; + textColor = pal.fillColor.text.primary; } const auto panel = Q::TextPanel | section | state; + const auto text = Q::Text | section | state; panelColor = rgbSolid( panelColor, pal.background.solid.base ); setGradient( panel, panelColor ); setBoxBorderGradient( panel, borderColor1, borderColor2, panelColor ); + + setColor( text, textColor ); + } + + for ( const auto subControl : { Q::Icon, Q::Button } ) + { + const auto aspect = subControl | section; + + setGraphicRole( aspect, W::GraphicRoleFillColorTextSecondary ); + setGraphicRole( aspect | Q::Disabled, W::GraphicRoleFillColorTextDisabled ); } } diff --git a/designsystems/fluent2/QskFluent2TextFieldSkinlet.cpp b/designsystems/fluent2/QskFluent2TextFieldSkinlet.cpp index 7c4c9b9b..f05cb44e 100644 --- a/designsystems/fluent2/QskFluent2TextFieldSkinlet.cpp +++ b/designsystems/fluent2/QskFluent2TextFieldSkinlet.cpp @@ -20,13 +20,41 @@ QskFluent2TextFieldSkinlet::~QskFluent2TextFieldSkinlet() QRectF QskFluent2TextFieldSkinlet::subControlRect( const QskSkinnable* skinnable, const QRectF& contentsRect, QskAspect::Subcontrol subControl ) const { + if ( subControl == Q::TextPanel ) + { + auto rect = subControlRect( skinnable, contentsRect, Q::Panel ); + rect.setY( rect.bottom() - skinnable->strutSizeHint( subControl ).height() ); + + return rect; + } + + if ( subControl == Q::Header ) + { + const auto rect = subControlRect( skinnable, contentsRect, Q::TextPanel ); + const auto h = skinnable->effectiveFontHeight( Q::Header ); + + return QRectF( rect.x(), rect.y() - h, rect.width(), h ); + } + return Inherited::subControlRect( skinnable, contentsRect, subControl ); } QSizeF QskFluent2TextFieldSkinlet::sizeHint( const QskSkinnable* skinnable, Qt::SizeHint which, const QSizeF& constraint ) const { - return Inherited::sizeHint( skinnable, which, constraint ); + if ( which != Qt::PreferredSize ) + return QSizeF(); + + auto hint = Inherited::sizeHint( skinnable, which, constraint ); + + const auto textField = static_cast< const QskTextField* >( skinnable ); + if ( !textField->headerText().isEmpty() ) + { + // spacing ??? + hint.rheight() += textField->strutSizeHint( Q::Header ).height(); + } + + return hint; } #include "moc_QskFluent2TextFieldSkinlet.cpp" diff --git a/designsystems/fluent2/icons/dismiss.svg b/designsystems/fluent2/icons/dismiss.svg new file mode 100644 index 00000000..ea678912 --- /dev/null +++ b/designsystems/fluent2/icons/dismiss.svg @@ -0,0 +1,3 @@ + + + diff --git a/designsystems/fluent2/icons/qvg/dismiss.qvg b/designsystems/fluent2/icons/qvg/dismiss.qvg new file mode 100644 index 00000000..0cce0099 Binary files /dev/null and b/designsystems/fluent2/icons/qvg/dismiss.qvg differ diff --git a/designsystems/fluent2/icons/qvg/search.qvg b/designsystems/fluent2/icons/qvg/search.qvg new file mode 100644 index 00000000..48e33a09 Binary files /dev/null and b/designsystems/fluent2/icons/qvg/search.qvg differ diff --git a/designsystems/fluent2/icons/search.svg b/designsystems/fluent2/icons/search.svg new file mode 100644 index 00000000..ebdd5c29 --- /dev/null +++ b/designsystems/fluent2/icons/search.svg @@ -0,0 +1,3 @@ + + + diff --git a/designsystems/material3/QskMaterial3Icons.qrc b/designsystems/material3/QskMaterial3Icons.qrc index 72725b7a..2c44364a 100644 --- a/designsystems/material3/QskMaterial3Icons.qrc +++ b/designsystems/material3/QskMaterial3Icons.qrc @@ -17,5 +17,8 @@ icons/qvg/arrow_drop_up.qvg icons/qvg/check.qvg icons/qvg/remove.qvg + icons/qvg/text_field_search.qvg + icons/qvg/text_field_cancel.qvg + icons/qvg/text_field_error.qvg diff --git a/designsystems/material3/QskMaterial3Skin.cpp b/designsystems/material3/QskMaterial3Skin.cpp index c4b78be1..5d2c5ef5 100644 --- a/designsystems/material3/QskMaterial3Skin.cpp +++ b/designsystems/material3/QskMaterial3Skin.cpp @@ -478,43 +478,161 @@ void Editor::setupTextArea() void Editor::setupTextField() { using Q = QskTextField; + using M3 = QskMaterial3Skin; + using A = QskAspect; setColor( Q::Text, m_pal.onSurface ); setFontRole( Q::Text, BodyLarge ); setColor( Q::Text | Q::Disabled, m_pal.onSurface38 ); - setStrutSize( Q::Panel, -1.0, 56_px ); - setPadding( Q::Panel, { 12_px, 8_px, 12_px, 8_px } ); - setGradient( Q::Panel, m_pal.surfaceVariant ); - setColor( Q::TextPanel | Q::Selected, m_pal.primary12 ); - setBoxShape( Q::Panel, m_pal.shapeExtraSmallTop ); - setBoxBorderMetrics( Q::Panel, { 0, 0, 0, 1_px } ); - setBoxBorderColors( Q::Panel, m_pal.onSurfaceVariant ); - setSpacing( Q::Panel, 8_px ); + const auto Outlined = static_cast< A::Variation >( Q::OutlinedStyle ); + const auto Filled = static_cast< A::Variation >( Q::FilledStyle ); - const auto hoverColor = flattenedColor( m_pal.onSurfaceVariant, - m_pal.surfaceVariant, m_pal.hoverOpacity ); - setGradient( Q::Panel | Q::Hovered, hoverColor ); + const auto activeStates = Q::Focused | Q::Editing; - const auto focusColor = flattenedColor( m_pal.onSurfaceVariant, - m_pal.surfaceVariant, m_pal.focusOpacity ); - setGradient( Q::Panel | Q::Focused, focusColor ); + { + // Text - // ### Also add a pressed state + setAnimation( Q::TextPanel | A::Color, qskDuration ); + setAnimation( Q::TextPanel | A::Metric, qskDuration ); + } - setFontRole( Q::Text, BodyLarge ); + for ( const auto variation : { A::NoVariation, Filled, Outlined } ) + { + const auto Panel = Q::Panel | variation; - setAlignment( Q::Placeholder, Qt::AlignLeft | Qt::AlignVCenter ); + QskBoxBorderMetrics borderMetrics[2]; - const auto disabledPanelColor = QskRgb::toTransparentF( m_pal.onSurface, 0.04 ); - setGradient( Q::Panel | Q::Disabled, disabledPanelColor ); - setBoxBorderColors( Q::Panel | Q::Disabled, m_pal.onSurface38 ); + if ( variation == Filled ) + { + setBoxShape( Panel, m_pal.shapeExtraSmallTop ); - // PlaceholderText + borderMetrics[0].setBottom( 1 ); + borderMetrics[1].setBottom( 2 ); - setColor( Q::Placeholder, color( Q::Text ) ); - setFontRole( Q::Placeholder, BodyLarge ); - setAlignment( Q::Placeholder, Qt::AlignLeft | Qt::AlignVCenter ); + setBoxBorderColors( Panel, m_pal.onSurfaceVariant ); + + setGradient( Panel, m_pal.surfaceVariant ); + + setGradient( Panel | Q::Hovered, + m_pal.hoverColor( m_pal.onSurfaceVariant, m_pal.surfaceVariant ), + { QskStateCombination::CombinationNoState, activeStates | Q::Error } ); + + setGradient( Panel | Q::Disabled, + QskRgb::toTransparentF( m_pal.onSurface, 0.04 ) ); + } + else + { + setBoxShape( Panel, m_pal.shapeExtraSmall ); + + borderMetrics[0].setWidths( 1 ); + borderMetrics[1].setWidths( 2 ); + + setBoxBorderColors( Panel, m_pal.outline ); + } + + if ( variation != A::NoVariation ) + { + setStrutSize( Panel, -1.0, 56_px ); + setPadding( Panel, 16_px, 8_px, 16_px, 8_px ); + } + + setBoxBorderMetrics( Panel, borderMetrics[0] ); + setBoxBorderMetrics( Panel, borderMetrics[1], activeStates | Q::Hovered ); + setBoxBorderMetrics( Panel | Q::Error, borderMetrics[1], activeStates | Q::Hovered ); + + setBoxBorderColors( Panel, m_pal.primary, activeStates ); + setBoxBorderColors( Panel | Q::Hovered, m_pal.primary, activeStates ); + setBoxBorderColors( Panel | Q::Hovered, m_pal.onSurface ); + setBoxBorderColors( Panel | Q::Disabled, m_pal.onSurface38 ); + + setBoxBorderColors( Panel | Q::Error, m_pal.error, + { QskStateCombination::CombinationNoState, activeStates | Q::Hovered } ); + + setColor( Q::TextPanel | variation | Q::Selected, m_pal.primary12 ); + } + + // Icon + + setStrutSize( Q::Icon, { 24_px, 24_px } ); + setMargin( Q::Icon, 2_px ); + setSymbol( Q::Icon, symbol( "text_field_search" ) ); + + setGraphicRole( Q::Icon, M3::GraphicRoleOnSurface ); + setGraphicRole( Q::Icon | Q::Error, M3::GraphicRoleOnSurfaceVariant ); + + setGraphicRole( Q::Icon | Q::Disabled, M3::GraphicRoleOnSurface38 ); + + { + setAlignment( Q::Header, Qt::AlignLeft | Qt::AlignVCenter ); + setFontRole( Q::Header, BodySmall ); + + setColor( Q::Header, m_pal.onSurfaceVariant ); + setColor( Q::Header, m_pal.primary, activeStates ); + setColor( Q::Header | Q::Error, m_pal.error ); + setColor( Q::Header | Q::Disabled, m_pal.onSurface38 ); + } + +#if 0 + setMargin( Q::Header | Outlined, 4_px, 0, 4_px, 0 ); +#endif + + for ( const auto subControl : { Q::Text, Q::Placeholder } ) + { + setAlignment( subControl, Qt::AlignLeft | Qt::AlignVCenter ); + + setFontRole( subControl, BodyLarge ); + + setColor( subControl | Q::Disabled, m_pal.onSurface38 ); + + if ( subControl == Q::Text ) + { + setColor( subControl, m_pal.onSurface ); + } + else + { + setColor( subControl | Q::Error, m_pal.error ); + setColor( subControl | Q::Error | Q::Hovered, m_pal.onSurface ); + } + } + + // Button + + setStrutSize( Q::Button, { 24_px, 24_px } ); + setMargin( Q::Button, 2_px ); + setGraphicRole( Q::Button, M3::GraphicRoleOnSurfaceVariant ); + setSymbol( Q::Button, symbol( "text_field_cancel" ) ); + + setSymbol( Q::Button | Q::Error, symbol( "text_field_error" ) ); + setGraphicRole( Q::Button | Q::Error, M3::GraphicRoleError ); + setGraphicRole( Q::Button | Q::Error | Q::Hovered, M3::GraphicRoleOnErrorContainer ); + + setGraphicRole( Q::Button | Q::Disabled, M3::GraphicRoleOnSurface38 ); + + // ButtonPanel + + setStrutSize( Q::ButtonPanel, { 45_px, 45_px } ); + setGradient( Q::ButtonPanel | Q::Hovered, m_pal.onSurface8 ); + setBoxShape( Q::ButtonPanel, 100, Qt::RelativeSize ); + + + // SupportingText + + setMargin( Q::Footer, { 16_px, 4_px, 16_px, 4_px } ); + setColor( Q::Footer, m_pal.onSurfaceVariant ); + setColor( Q::Footer | Q::Error, m_pal.error ); + setFontRole( Q::Footer, BodySmall ); + setAlignment( Q::Footer, Qt::AlignLeft | Qt::AlignVCenter ); + + setColor( Q::Footer | Q::Disabled, m_pal.onSurface38 ); + + // CharacterCount + + setMargin( Q::CharacterCount, margin( Q::Footer ) ); + setColor( Q::CharacterCount, color( Q::Footer ) ); + setFontRole( Q::CharacterCount, fontRole( Q::Footer ) ); + setAlignment( Q::CharacterCount, Qt::AlignRight | Qt::AlignVCenter ); + setColor( Q::CharacterCount | Q::Disabled, color( Q::Footer | Q::Disabled ) ); } void Editor::setupProgressBar() @@ -1619,6 +1737,7 @@ QskMaterial3Theme::QskMaterial3Theme( QskSkin::ColorScheme colorScheme, elevation2 = QskShadowMetrics( -2, 8, { 0, 2 } ); elevation3 = QskShadowMetrics( -1, 11, { 0, 2 } ); + shapeExtraSmall = QskBoxShapeMetrics( 4_px, 4_px, 4_px, 4_px ); shapeExtraSmallTop = QskBoxShapeMetrics( 4_px, 4_px, 0, 0 ); } @@ -1745,6 +1864,7 @@ void QskMaterial3Skin::setupGraphicFilters( const QskMaterial3Theme& theme ) setGraphicColor( GraphicRoleOnPrimaryContainer, theme.onPrimaryContainer ); setGraphicColor( GraphicRoleOnSecondaryContainer, theme.onSecondaryContainer ); setGraphicColor( GraphicRoleOnError, theme.onError ); + setGraphicColor( GraphicRoleOnErrorContainer, theme.onErrorContainer ); setGraphicColor( GraphicRoleOnSurface, theme.onSurface ); setGraphicColor( GraphicRoleOnSurface38, theme.onSurface38 ); setGraphicColor( GraphicRoleOnSurfaceVariant, theme.onSurfaceVariant ); diff --git a/designsystems/material3/QskMaterial3Skin.h b/designsystems/material3/QskMaterial3Skin.h index a1063d65..8042dda5 100644 --- a/designsystems/material3/QskMaterial3Skin.h +++ b/designsystems/material3/QskMaterial3Skin.h @@ -107,6 +107,7 @@ class QSK_MATERIAL3_EXPORT QskMaterial3Theme qreal stateOpacity( int state ) const; + QskBoxShapeMetrics shapeExtraSmall; QskBoxShapeMetrics shapeExtraSmallTop; }; @@ -119,7 +120,9 @@ class QSK_MATERIAL3_EXPORT QskMaterial3Skin : public QskSkin public: enum GraphicRole { + GraphicRoleError, GraphicRoleOnError, + GraphicRoleOnErrorContainer, GraphicRoleOnPrimary, GraphicRoleOnPrimaryContainer, GraphicRoleOnSecondaryContainer, diff --git a/designsystems/material3/QskMaterial3TextFieldSkinlet.cpp b/designsystems/material3/QskMaterial3TextFieldSkinlet.cpp index fb0882a8..0cd3d3d1 100644 --- a/designsystems/material3/QskMaterial3TextFieldSkinlet.cpp +++ b/designsystems/material3/QskMaterial3TextFieldSkinlet.cpp @@ -4,11 +4,82 @@ *****************************************************************************/ #include "QskMaterial3TextFieldSkinlet.h" -#include "QskTextField.h" +#include "QskMaterial3Skin.h" + +#include +#include +#include +#include + +#include + +using Q = QskTextField; + +namespace +{ + const int spacingV = 0; // skin hint ! + + QString effectiveHeaderText( const QskTextField* textField ) + { + if ( !textField->isEditing() && textField->text().isEmpty() ) + return QString(); + + return textField->headerText(); + } + + inline bool hasCharacterCount( const QskTextField* textField ) + { + // magic number hardcoded in qquicktextinput.cpp + return textField->maxLength() < 32767; + } + + inline bool hasBottomText( const QskTextField* textField ) + { + return !textField->footerText().isEmpty() || hasCharacterCount( textField ); + } + + QString maxLengthString( const QskTextField* textField ) + { + QString s = QString::number( textField->text().length() ) + + " / " + QString::number( textField->maxLength() ); + return s; + } + + // We need to "cut a hole" in the upper gradient for the label text: + QskBoxBorderColors outlineColors( const QskBoxBorderColors& colors, + const QRectF& rect, const QRectF& clipRect ) + { + auto gradient = colors.gradientAt( Qt::TopEdge ); + + const auto margin = 6; // ->skin + auto s1 = ( clipRect.left() - margin - rect.left() ) / rect.width(); + auto s2 = ( clipRect.right() - rect.left() ) / rect.width(); + + s1 = qBound( 0.0, s1, 1.0 ); + s2 = qBound( 0.0, s2, 1.0 ); + + // not correct, when gradient is not monochrome !!! + + gradient.setStops( { + { 0.0, gradient.startColor() }, + { s1, gradient.startColor() }, + { s1, Qt::transparent }, + { s2, Qt::transparent }, + { s2, gradient.endColor() }, + { 1.0, gradient.endColor() } + } ); + + auto borderColors = colors; + borderColors.setGradientAt( Qt::TopEdge, gradient ); + + return borderColors; + } +} QskMaterial3TextFieldSkinlet::QskMaterial3TextFieldSkinlet( QskSkin* skin ) : Inherited( skin ) { + appendNodeRoles( { SupportingTextRole, CharacterCountRole } ); } QskMaterial3TextFieldSkinlet::~QskMaterial3TextFieldSkinlet() @@ -18,19 +89,209 @@ QskMaterial3TextFieldSkinlet::~QskMaterial3TextFieldSkinlet() QRectF QskMaterial3TextFieldSkinlet::subControlRect( const QskSkinnable* skinnable, const QRectF& contentsRect, QskAspect::Subcontrol subControl ) const { + const auto textField = static_cast< const Q* >( skinnable ); + + if ( subControl == Q::Panel ) + { + auto rect = contentsRect; + + if( textField->style() == QskTextField::OutlinedStyle ) + { + const auto h = textField->effectiveFontHeight( Q::Header ); + rect.setTop( rect.top() + 0.5 * h ); + } + + if( hasBottomText( textField ) ) + { + const auto margins = textField->marginHint( Q::Footer ); + + const auto h = textField->effectiveFontHeight( Q::Footer ) + + margins.top() + margins.bottom(); + + rect.setHeight( rect.height() - h ); + } + + return rect; + } + + if ( subControl == Q::Text ) + { + auto rect = Inherited::subControlRect( skinnable, contentsRect, Q::Text ); + + if ( !rect.isEmpty() && ( textField->style() == QskTextField::FilledStyle ) ) + { + const auto text = effectiveHeaderText( textField ); + if ( !text.isEmpty() ) + { + const auto h = skinnable->effectiveFontHeight( Q::Header ); + rect.translate( 0.0, 0.5 * ( h + spacingV ) ); + } + } + + return rect; + } + + if ( subControl == Q::Header ) + { + const auto text = effectiveHeaderText( textField ); + if( text.isEmpty() ) + return QRectF(); + + const QFontMetrics fm( textField->effectiveFont( Q::Header ) ); + const auto textSize = fm.size( Qt::TextSingleLine | Qt::TextExpandTabs, text ); + + qreal x, y; + + if ( textField->style() == QskTextField::FilledStyle ) + { + const auto r = subControlRect( skinnable, contentsRect, Q::Text ); + + x = r.left(); + y = r.top() - spacingV - textSize.height(); + } + else if ( textField->style() == QskTextField::OutlinedStyle ) + { + const auto r = subControlRect( skinnable, contentsRect, Q::Panel ); + + x = r.left() + skinnable->paddingHint( Q::Panel ).left(); + y = r.top() - 0.5 * textSize.height(); + } + + return QRectF( x, y, textSize.width(), textSize.height() ); + } + + if ( subControl == Q::Footer ) + { + if( !textField->footerText().isEmpty() ) + { + auto rect = contentsRect; + + const auto margins = textField->marginHint( subControl ); + const auto h = textField->effectiveFontHeight( subControl ) + + margins.top() + margins.bottom(); + + rect.setTop( rect.bottom() - h ); + rect.setLeft( rect.left() + margins.left() ); + + return rect; + } + + return QRectF(); + } + + if ( subControl == Q::CharacterCount ) + { + if( hasCharacterCount( textField ) ) + { + auto rect = contentsRect; + + const auto margins = textField->marginHint( subControl ); + const auto h = textField->effectiveFontHeight( subControl ) + + margins.top() + margins.bottom(); + + rect.setTop( rect.bottom() - h ); + + const QFontMetricsF fm( textField->effectiveFont( subControl ) ); + const auto w = qskHorizontalAdvance( fm, maxLengthString( textField ) ); + rect.setRight( rect.right() - margins.right() ); + rect.setLeft( rect.right() - ( margins.left() + w + margins.right() ) ); + + return rect; + } + + return QRectF(); + } + return Inherited::subControlRect( skinnable, contentsRect, subControl ); } QSGNode* QskMaterial3TextFieldSkinlet::updateSubNode( const QskSkinnable* skinnable, quint8 nodeRole, QSGNode* node ) const { + const auto textField = static_cast< const Q* >( skinnable ); + + switch ( nodeRole ) + { + case PanelRole: + { + if( ( textField->style() == QskTextField::OutlinedStyle ) && + !effectiveHeaderText( textField ).isEmpty() ) + { + auto clipRect = textField->subControlRect( Q::Header ); + if ( !clipRect.isEmpty() ) + { + const auto subControl = Q::Panel; + + const auto panelRect = textField->subControlRect( subControl ); + + auto borderColors = textField->boxBorderColorsHint( subControl ); + borderColors = outlineColors( borderColors, panelRect, clipRect ); + + return updateBoxNode( skinnable, node, + panelRect, + skinnable->boxShapeHint( subControl ), + skinnable->boxBorderMetricsHint( subControl ), + borderColors, + skinnable->gradientHint( subControl ) ); + } + } + + return updateBoxNode( skinnable, node, Q::Panel ); + } + + case CharacterCountRole: + { + return updateTextNode( skinnable, node, + maxLengthString( textField ), Q::CharacterCount ); + } + + case HeaderRole: + { + return updateTextNode( skinnable, node, + effectiveHeaderText( textField ), Q::Header ); + } + } + return Inherited::updateSubNode( skinnable, nodeRole, node ); } QSizeF QskMaterial3TextFieldSkinlet::sizeHint( const QskSkinnable* skinnable, Qt::SizeHint which, const QSizeF& constraint ) const { - return Inherited::sizeHint( skinnable, which, constraint ); + if ( which != Qt::PreferredSize ) + return QSizeF(); + + auto hint = Inherited::sizeHint( skinnable, which, constraint ); + + const auto textField = static_cast< const QskTextField* >( skinnable ); + + if( textField->style() != QskTextField::PlainStyle ) + hint.rheight() += textField->effectiveFontHeight( Q::Header ) + spacingV; + + if( hasBottomText( textField ) ) + { + const auto margins = textField->marginHint( Q::Footer ); + hint.rheight() += textField->effectiveFontHeight( Q::Footer ) + + margins.top() + margins.bottom(); + } + + return hint; +} + +QString QskMaterial3TextFieldSkinlet::effectivePlaceholderText( + const QskTextField* textField ) const +{ + if ( textField->text().isEmpty() && + !( textField->isReadOnly() || textField->isEditing() ) ) + { + auto text = textField->placeholderText(); + if ( text.isEmpty() ) + text = textField->headerText(); + + return text; + } + + return QString(); } #include "moc_QskMaterial3TextFieldSkinlet.cpp" diff --git a/designsystems/material3/QskMaterial3TextFieldSkinlet.h b/designsystems/material3/QskMaterial3TextFieldSkinlet.h index 420d69fb..f9b35173 100644 --- a/designsystems/material3/QskMaterial3TextFieldSkinlet.h +++ b/designsystems/material3/QskMaterial3TextFieldSkinlet.h @@ -16,6 +16,12 @@ class QSK_MATERIAL3_EXPORT QskMaterial3TextFieldSkinlet : public QskTextFieldSki using Inherited = QskTextFieldSkinlet; public: + enum NodeRole : quint8 + { + SupportingTextRole = Inherited::RoleCount, + CharacterCountRole + }; + Q_INVOKABLE QskMaterial3TextFieldSkinlet( QskSkin* = nullptr ); ~QskMaterial3TextFieldSkinlet() override; @@ -28,6 +34,8 @@ class QSK_MATERIAL3_EXPORT QskMaterial3TextFieldSkinlet : public QskTextFieldSki protected: QSGNode* updateSubNode( const QskSkinnable*, quint8 nodeRole, QSGNode* ) const override; + + QString effectivePlaceholderText( const QskTextField* ) const override; }; #endif diff --git a/designsystems/material3/icons/qvg/text_field_cancel.qvg b/designsystems/material3/icons/qvg/text_field_cancel.qvg new file mode 100644 index 00000000..7607d209 Binary files /dev/null and b/designsystems/material3/icons/qvg/text_field_cancel.qvg differ diff --git a/designsystems/material3/icons/qvg/text_field_error.qvg b/designsystems/material3/icons/qvg/text_field_error.qvg new file mode 100644 index 00000000..013508ad Binary files /dev/null and b/designsystems/material3/icons/qvg/text_field_error.qvg differ diff --git a/designsystems/material3/icons/qvg/text_field_search.qvg b/designsystems/material3/icons/qvg/text_field_search.qvg new file mode 100644 index 00000000..b289e352 Binary files /dev/null and b/designsystems/material3/icons/qvg/text_field_search.qvg differ diff --git a/designsystems/material3/icons/text_field_cancel.svg b/designsystems/material3/icons/text_field_cancel.svg new file mode 100644 index 00000000..3c8de038 --- /dev/null +++ b/designsystems/material3/icons/text_field_cancel.svg @@ -0,0 +1,4 @@ + + + + diff --git a/designsystems/material3/icons/text_field_error.svg b/designsystems/material3/icons/text_field_error.svg new file mode 100644 index 00000000..3b7b117b --- /dev/null +++ b/designsystems/material3/icons/text_field_error.svg @@ -0,0 +1,4 @@ + + + + diff --git a/designsystems/material3/icons/text_field_search.svg b/designsystems/material3/icons/text_field_search.svg new file mode 100644 index 00000000..d10c97b6 --- /dev/null +++ b/designsystems/material3/icons/text_field_search.svg @@ -0,0 +1,4 @@ + + + + diff --git a/examples/gallery/inputs/InputPage.cpp b/examples/gallery/inputs/InputPage.cpp index 7cf72289..d79366ee 100644 --- a/examples/gallery/inputs/InputPage.cpp +++ b/examples/gallery/inputs/InputPage.cpp @@ -10,6 +10,12 @@ #include #include #include +#include +#include +#include +#include + +#include namespace { @@ -66,37 +72,62 @@ namespace { public: TextInputBox( QQuickItem* parent = nullptr ) - : QskLinearBox( Qt::Horizontal, parent ) + : QskLinearBox( Qt::Horizontal, 3, parent ) { setSpacing( 20 ); + setDefaultAlignment( Qt::AlignHCenter | Qt::AlignTop ); { - { - auto field = new QskTextField( this ); - field->setText( "John Doe" ); - field->setPlaceholderText( "" ); - -#if 0 - connect( field, &QskTextField::textChanged, - [field]() { qDebug() << "Text:" << field->text(); } ); -#endif - - } - - { - auto field = new QskTextField( this ); - field->setReadOnly( true ); - field->setText( "Read Only" ); - field->setSizePolicy( Qt::Horizontal, QskSizePolicy::MinimumExpanding ); - } - - { - auto field = new QskTextField( this ); - field->setMaxLength( 5 ); - field->setEchoMode( QskTextField::Password ); - field->setPlaceholderText( "" ); - } + auto field = new QskTextField( this ); + field->setHeaderText( "Name" ); + field->setText( "John Doe" ); + field->setPlaceholderText( "" ); + field->setFooterText( "Required *" ); } + + { + auto field = new QskTextField( this ); + field->setHeaderText( "Nickname" ); + field->setPlaceholderText( "" ); + field->setFooterText( "Optional" ); + } + { + auto field = new QskTextField( this ); + field->setIcon( {} ); + field->setPlaceholderText( "" ); + } + + { + auto field = new QskTextField( this ); + field->setSkinStateFlag( QskTextField::Error ); + field->setText( "Error Text" ); + field->setHeaderText( "error" ); + field->setPlaceholderText( "" ); + field->setFooterText( "error text" ); + } + + { + auto field = new QskTextField( this ); + field->setReadOnly( true ); + field->setText( "Read Only" ); + field->setHeaderText( "read only" ); + field->setSizePolicy( Qt::Horizontal, QskSizePolicy::MinimumExpanding ); + } + + { + auto field = new QskTextField( this ); + field->setMaxLength( 15 ); + field->setHeaderText( "password" ); + field->setEchoMode( QskTextField::Password ); + field->setPlaceholderText( "" ); + } + } + + void setStyle( int style ) + { + auto textFields = findChildren< QskTextField* >(); + for ( auto field : textFields ) + field->setStyle( static_cast< QskTextField::Style >( style ) ); } }; @@ -120,6 +151,18 @@ namespace } } }; + + class StyleComboBox : public QskComboBox + { + public: + StyleComboBox( QQuickItem* parent = nullptr ) + : QskComboBox( parent ) + { + addOption( QString(), "Plain" ); + addOption( QString(), "Outlined" ); + addOption( QString(), "Filled" ); + } + }; } InputPage::InputPage( QQuickItem* parent ) @@ -146,30 +189,44 @@ InputPage::InputPage( QQuickItem* parent ) } auto spinBox = new QskSpinBox( 0.0, 100.0, 1.0 ); + spinBox->setObjectName( "SliderValueSpinBox" ); spinBox->setSizePolicy( Qt::Horizontal, QskSizePolicy::Fixed ); + spinBox->setLayoutAlignmentHint( Qt::AlignCenter ); auto textInputBox = new TextInputBox(); textInputBox->setSizePolicy( Qt::Vertical, QskSizePolicy::Fixed ); auto textAreaBox = new TextAreaBox(); - auto vBox = new QskLinearBox( Qt::Vertical ); - vBox->setSpacing( 30 ); - vBox->setExtraSpacingAt( Qt::RightEdge | Qt::BottomEdge ); + auto separator = new QskSeparator(); + separator->setMargins( 5, 20, 5, 10 ); + auto styleBox = new StyleComboBox(); + + auto vBox = new QskLinearBox( Qt::Vertical ); + vBox->setSpacing( 20 ); vBox->addItem( sliders[0].continous ); vBox->addItem( sliders[0].discrete ); vBox->addItem( sliders[0].centered ); vBox->addItem( spinBox ); vBox->addItem( textInputBox ); - vBox->addItem( textAreaBox ); - auto mainBox = new QskLinearBox( Qt::Horizontal, this ); - mainBox->setSpacing( 30 ); - mainBox->addItem( sliders[1].continous ); - mainBox->addItem( sliders[1].discrete ); - mainBox->addItem( sliders[1].centered ); - mainBox->addItem( vBox ); + auto hBox = new QskLinearBox( Qt::Horizontal ); + hBox->setSpacing( 20 ); + hBox->addItem( sliders[1].continous ); + hBox->addItem( sliders[1].discrete ); + hBox->addItem( sliders[1].centered ); + + auto gridBox = new QskGridBox( this ); + gridBox->addItem( spinBox, 0, 0 ); + gridBox->addItem( hBox, 1, 0, -1, 1 ); + gridBox->addItem( vBox, 0, 1, 1, -1 ); + gridBox->addItem( separator, 1, 1 ); + gridBox->addItem( styleBox, 1, 2 ); + gridBox->addItem( textInputBox, 2, 1, 1, -1 ); + gridBox->addItem( textAreaBox, 3, 1, 1, -1 ); + gridBox->setRowStretchFactor( 3, 10 ); + gridBox->setColumnStretchFactor( 1, 10 ); auto inputs = findChildren< QskBoundedValueInput* >(); @@ -180,6 +237,11 @@ InputPage::InputPage( QQuickItem* parent ) } spinBox->setValue( 30.0 ); + + connect( styleBox, &QskComboBox::currentIndexChanged, + textInputBox, &TextInputBox::setStyle ); + + styleBox->setCurrentIndex( QskTextField::OutlinedStyle ); } void InputPage::syncValues( qreal value ) @@ -193,8 +255,7 @@ void InputPage::syncValues( qreal value ) if ( qobject_cast< const QskSlider* >( sender() ) ) { - auto spinBoxes = findChildren< QskSpinBox* >(); - for ( auto spinBox : spinBoxes ) + if ( auto spinBox = findChild< QskSpinBox* >( "SliderValueSpinBox" ) ) spinBox->setValue( value ); } else diff --git a/src/controls/QskTextField.cpp b/src/controls/QskTextField.cpp index 904a3c6d..23355ab5 100644 --- a/src/controls/QskTextField.cpp +++ b/src/controls/QskTextField.cpp @@ -4,20 +4,43 @@ *****************************************************************************/ #include "QskTextField.h" +#include "QskEvent.h" +#include "QskFontRole.h" +#include "QskQuick.h" QSK_SUBCONTROL( QskTextField, Panel ) + +QSK_SUBCONTROL( QskTextField, Header ) +QSK_SUBCONTROL( QskTextField, Footer ) + +QSK_SUBCONTROL( QskTextField, Icon ) +QSK_SUBCONTROL( QskTextField, ButtonPanel ) +QSK_SUBCONTROL( QskTextField, Button ) QSK_SUBCONTROL( QskTextField, Placeholder ) +QSK_SUBCONTROL( QskTextField, CharacterCount ) + +QSK_SYSTEM_STATE( QskTextField, Pressed, QskAspect::LastUserState << 1 ) + class QskTextField::PrivateData { public: + QString headerText; + QString footerText; QString placeholderText; + + Style style = PlainStyle; + QskAspect::States buttonStates; }; QskTextField::QskTextField( QQuickItem* parent ) : Inherited( parent ) , m_data( new PrivateData() ) { +#if 1 + // character count might have changed + connect( this, &QskTextInput::textChanged, this, &QQuickItem::update ); +#endif } QskTextField::QskTextField( const QString& text, QQuickItem* parent ) @@ -30,6 +53,74 @@ QskTextField::~QskTextField() { } +void QskTextField::setStyle( Style style ) +{ + if ( style != m_data->style ) + { + m_data->style = style; + + resetImplicitSize(); + update(); + + Q_EMIT styleChanged( style ); + } +} + +QskTextField::Style QskTextField::style() const +{ + return m_data->style; +} + +QString QskTextField::headerText() const +{ + return m_data->headerText; +} + +void QskTextField::setHeaderText( const QString& text ) +{ + if ( m_data->headerText != text ) + { + m_data->headerText = text; + + update(); + resetImplicitSize(); + + Q_EMIT headerTextChanged( text ); + } +} + +QString QskTextField::footerText() const +{ + return m_data->footerText; +} + +void QskTextField::setFooterText( const QString& text ) +{ + if ( m_data->footerText != text ) + { + m_data->footerText = text; + + update(); + resetImplicitSize(); + + Q_EMIT footerTextChanged( text ); + } +} + +QskGraphic QskTextField::icon() const +{ + return symbolHint( Icon ); +} + +void QskTextField::setIcon( const QskGraphic& icon ) +{ + if ( setSymbolHint( Icon, icon ) ) + { + update(); + resetImplicitSize(); + } +} + void QskTextField::setPlaceholderText( const QString& text ) { if ( m_data->placeholderText != text ) @@ -44,4 +135,106 @@ QString QskTextField::placeholderText() const return m_data->placeholderText; } +QskAspect::Variation QskTextField::effectiveVariation() const +{ + return static_cast< QskAspect::Variation >( m_data->style ); +} + +void QskTextField::handleButtonClick() +{ + clear(); + setEditing( true ); +} + +void QskTextField::mousePressEvent( QMouseEvent* event ) +{ + if( !isReadOnly() ) + { + const auto r = subControlRect( Button ); + if ( r.contains( qskMousePosition( event ) ) ) + { + setButtonState( Pressed, true ); + return; + } + } + + Inherited::mousePressEvent( event ); +} + +void QskTextField::mouseMoveEvent( QMouseEvent* event ) +{ + if ( m_data->buttonStates & Pressed ) + { + const auto r = subControlRect( Button ); + setButtonState( Pressed, r.contains( qskMousePosition( event ) ) ); + return; + } + + Inherited::mouseMoveEvent( event ); +} + +void QskTextField::mouseReleaseEvent( QMouseEvent* event ) +{ + if ( m_data->buttonStates & Pressed ) + { + setButtonState( Pressed, false ); + handleButtonClick(); + + return; + } + + Inherited::mouseReleaseEvent( event ); +} + +void QskTextField::mouseUngrabEvent() +{ + setButtonState( Pressed, false ); + Inherited::mouseUngrabEvent(); +} + +void QskTextField::hoverEnterEvent( QHoverEvent* event ) +{ + Inherited::hoverEnterEvent( event ); + + const auto r = subControlRect( Button ); + setButtonState( Hovered, r.contains( qskHoverPosition( event ) ) ); +} + +void QskTextField::hoverMoveEvent( QHoverEvent* event ) +{ + const auto r = subControlRect( Button ); + setButtonState( Hovered, r.contains( qskHoverPosition( event ) ) ); + + Inherited::hoverMoveEvent( event ); +} + +void QskTextField::hoverLeaveEvent( QHoverEvent* event ) +{ + setButtonState( Hovered, false ); + Inherited::hoverLeaveEvent( event ); +} + +QskAspect::States QskTextField::buttonStates() const +{ + auto states = skinStates() | m_data->buttonStates; + + if ( !( m_data->buttonStates & Hovered ) ) + states &= ~Hovered; + + return states; +} + +void QskTextField::setButtonState( QskAspect::State state, bool on ) +{ + const auto oldStates = m_data->buttonStates; + + if ( on ) + m_data->buttonStates |= state; + else + m_data->buttonStates &= ~state; + + if ( oldStates != m_data->buttonStates ) + update(); +} + #include "moc_QskTextField.cpp" diff --git a/src/controls/QskTextField.h b/src/controls/QskTextField.h index e2e1f96a..139c61a8 100644 --- a/src/controls/QskTextField.h +++ b/src/controls/QskTextField.h @@ -7,31 +7,88 @@ #define QSK_TEXT_FIELD_H #include "QskTextInput.h" +#include "QskGraphic.h" class QSK_EXPORT QskTextField : public QskTextInput { Q_OBJECT + Q_PROPERTY( QString headerText READ headerText + WRITE setHeaderText NOTIFY headerTextChanged ) + + Q_PROPERTY( QString footerText READ footerText + WRITE setFooterText NOTIFY footerTextChanged ) + Q_PROPERTY( QString placeholderText READ placeholderText WRITE setPlaceholderText NOTIFY placeholderTextChanged ) + Q_PROPERTY( Style style READ style + WRITE setStyle NOTIFY styleChanged ) + using Inherited = QskTextInput; public: - QSK_SUBCONTROLS( Panel, Placeholder ) + QSK_STATES( Pressed ) + + QSK_SUBCONTROLS( Panel, Header, Footer, Placeholder, + Icon, Button, ButtonPanel, CharacterCount ) + + enum Style : quint8 + { + PlainStyle, + + OutlinedStyle, + FilledStyle + }; + Q_ENUM( Style ) QskTextField( QQuickItem* parent = nullptr ); QskTextField( const QString& text, QQuickItem* parent = nullptr ); ~QskTextField() override; + void setStyle( Style ); + Style style() const; + + void setHeaderText( const QString& ); + QString headerText() const; + + void setFooterText( const QString& ); + QString footerText() const; + + QskGraphic icon() const; + void setIcon( const QskGraphic& ); + void setPlaceholderText( const QString& ); QString placeholderText() const; + QskAspect::Variation effectiveVariation() const override; + +#if 1 + QskAspect::States buttonStates() const; +#endif + Q_SIGNALS: + void headerTextChanged( const QString& ); + void footerTextChanged( const QString& ); void placeholderTextChanged( const QString& ); + void styleChanged( Style ); + + protected: + void hoverEnterEvent( QHoverEvent* ) override; + void hoverMoveEvent( QHoverEvent* ) override; + void hoverLeaveEvent( QHoverEvent* ) override; + + void mousePressEvent( QMouseEvent* ) override; + void mouseMoveEvent( QMouseEvent* ) override; + void mouseReleaseEvent( QMouseEvent* ) override; + void mouseUngrabEvent() override; + + virtual void handleButtonClick(); private: + void setButtonState( QskAspect::State, bool ); + class PrivateData; std::unique_ptr< PrivateData > m_data; }; diff --git a/src/controls/QskTextFieldSkinlet.cpp b/src/controls/QskTextFieldSkinlet.cpp index b070e580..7b1e4e70 100644 --- a/src/controls/QskTextFieldSkinlet.cpp +++ b/src/controls/QskTextFieldSkinlet.cpp @@ -6,12 +6,16 @@ #include "QskTextFieldSkinlet.h" #include "QskTextField.h" +#include + using Q = QskTextField; QskTextFieldSkinlet::QskTextFieldSkinlet( QskSkin* skin ) : Inherited( skin ) { - setNodeRoles( { PanelRole, TextPanelRole, PlaceholderRole } ); + setNodeRoles( { PanelRole, TextPanelRole, + IconRole, ButtonPanelRole, ButtonRole, + PlaceholderRole, HeaderRole, FooterRole } ); } QskTextFieldSkinlet::~QskTextFieldSkinlet() @@ -28,7 +32,30 @@ QRectF QskTextFieldSkinlet::subControlRect( const QskSkinnable* skinnable, return skinnable->subControlContentsRect( contentsRect, Q::Panel ); if ( subControl == Q::Text ) - return skinnable->subControlContentsRect( contentsRect, Q::TextPanel ); + { + auto rect = skinnable->subControlContentsRect( contentsRect, Q::TextPanel ); + + if( !skinnable->symbolHint( Q::Icon ).isEmpty() ) + { + const auto r = subControlRect( skinnable, contentsRect, Q::Icon ); + if ( !r.isEmpty() ) + rect.setLeft( r.right() ); + } + + if( !skinnable->symbolHint( Q::Button ).isEmpty() ) + { + const auto r = subControlRect( skinnable, contentsRect, Q::Button ); + if( !r.isEmpty() ) + rect.setRight( r.left() ); + } + + const auto h = skinnable->effectiveFontHeight( Q::Text ); + rect.setTop( rect.center().y() - 0.5 * h ); + rect.setHeight( h ); + rect = rect.marginsAdded( skinnable->marginHint( Q::Text ) ); + + return rect; + } if ( subControl == Q::Placeholder ) { @@ -39,6 +66,60 @@ QRectF QskTextFieldSkinlet::subControlRect( const QskSkinnable* skinnable, return QRectF(); } + if ( subControl == Q::Icon ) + { + if( !skinnable->symbolHint( subControl ).isEmpty() ) + { + const auto panelRect = skinnable->subControlContentsRect( + contentsRect, Q::TextPanel ); + + auto rect = panelRect; + + rect.setSize( skinnable->strutSizeHint( subControl ) ); + rect.moveCenter( { rect.center().x(), panelRect.center().y() } ); + + return rect; + } + + return QRectF(); + } + + if ( subControl == Q::ButtonPanel ) + { + const auto textField = static_cast< const QskTextField* >( skinnable ); + if ( textField->buttonStates() & Q::Hovered ) + { + const auto r = subControlRect( skinnable, contentsRect, Q::Button ); + + QRectF rect( QPointF(), skinnable->strutSizeHint( subControl ) ); + rect.moveCenter( r.center() ); + + return rect; + } + + return QRectF(); + } + + if ( subControl == Q::Button ) + { + if( !skinnable->symbolHint( subControl ).isEmpty() ) + { + const auto panelRect = skinnable->subControlContentsRect( + contentsRect, Q::TextPanel ); + + auto rect = panelRect; + + const auto size = skinnable->strutSizeHint( subControl ); + rect.setHeight( size.height() ); + rect.moveCenter( { rect.center().x(), panelRect.center().y() } ); + rect.setLeft( rect.right() - size.width() ); + + return rect; + } + + return QRectF(); + } + return Inherited::subControlRect( skinnable, contentsRect, subControl ); } @@ -76,6 +157,27 @@ QSGNode* QskTextFieldSkinlet::updateSubNode( textField->alignmentHint( subControl, Qt::AlignLeft ), options, text, subControl ); } + + case HeaderRole: + { + return updateTextNode( skinnable, node, + textField->headerText(), Q::Header ); + } + + case FooterRole: + { + return updateTextNode( skinnable, node, + textField->footerText(), Q::Footer ); + } + + case IconRole: + return updateSymbolNode( skinnable, node, Q::Icon ); + + case ButtonPanelRole: + return updateBoxNode( skinnable, node, Q::ButtonPanel ); + + case ButtonRole: + return updateSymbolNode( skinnable, node, Q::Button ); } return Inherited::updateSubNode( skinnable, nodeRole, node ); diff --git a/src/controls/QskTextFieldSkinlet.h b/src/controls/QskTextFieldSkinlet.h index d5cb3b7d..14380363 100644 --- a/src/controls/QskTextFieldSkinlet.h +++ b/src/controls/QskTextFieldSkinlet.h @@ -21,7 +21,14 @@ class QSK_EXPORT QskTextFieldSkinlet : public QskSkinlet { PanelRole, TextPanelRole, + + HeaderRole, + FooterRole, PlaceholderRole, + IconRole, + ButtonPanelRole, + ButtonRole, + RoleCount };