From 58581477a7c41cd2d163b306ae8c8fe0a04de9d2 Mon Sep 17 00:00:00 2001 From: Michael Ragazzon Date: Sat, 6 Jul 2024 22:00:58 +0200 Subject: [PATCH] Make the client area always tied to the padding area, textarea elements now clip to their padding area This removes the support for customizing the client area of an element, and fixes it to its padding area. This makes it easier to understand what the client area is, and is also in line with how CSS defines this term. Previously, the client area was also used to define how the element was clipped. Instead, this is now made more explicit with a separate "clip area" that can be customized for each element. Normally, clipping occurs on the padding area of the element. The clip area is introduced mainly for single-line text inputs, as we still want to clip to the content area here. On the other hand, textarea elements previously also clipped to the content area, but this is undesirable and not according to CSS specifications. Now, the textarea elements clip to padding, just like normal elements. The 'invaders.rcss' textarea decorators previously assumed clipping to the content area. This would now lead to content being placed on top of the decorator when scrolling, which looks strange. Instead, span the decorator across the border-area of the element, and make a transparent border as a placeholder. This better describes the nature of the decorator, since after all it attempts to display a border. Since clipping now acts on the padding area, the content will be clipped when it is scrolled into the border area, where the decorator now displays the borders. Furthermore, adjust the position of the textarea scrollbars accordingly, and improve their looks generally. --- Include/RmlUi/Core/Element.h | 34 ++++++------ Samples/assets/invader.rcss | 55 +++++++++++++++---- Source/Core/Element.cpp | 18 +++--- Source/Core/ElementUtilities.cpp | 10 ++-- Source/Core/Elements/WidgetTextInput.cpp | 40 +++++++++----- Source/Core/Elements/WidgetTextInput.h | 5 ++ .../Elements/WidgetTextInputSingleLine.cpp | 7 ++- 7 files changed, 110 insertions(+), 59 deletions(-) diff --git a/Include/RmlUi/Core/Element.h b/Include/RmlUi/Core/Element.h index 0d0499aab..181c0552e 100644 --- a/Include/RmlUi/Core/Element.h +++ b/Include/RmlUi/Core/Element.h @@ -129,12 +129,12 @@ class RMLUICORE_API Element : public ScriptInterface, public EnableObserverPtrscroll.GetScrollbarSize(ElementScroll::VERTICAL); + return GetBox().GetSize(BoxArea::Padding).x - meta->scroll.GetScrollbarSize(ElementScroll::VERTICAL); } float Element::GetClientHeight() { - return GetBox().GetSize(client_area).y - meta->scroll.GetScrollbarSize(ElementScroll::HORIZONTAL); + return GetBox().GetSize(BoxArea::Padding).y - meta->scroll.GetScrollbarSize(ElementScroll::HORIZONTAL); } Element* Element::GetOffsetParent() diff --git a/Source/Core/ElementUtilities.cpp b/Source/Core/ElementUtilities.cpp index 22682401e..850113049 100644 --- a/Source/Core/ElementUtilities.cpp +++ b/Source/Core/ElementUtilities.cpp @@ -160,7 +160,7 @@ float ElementUtilities::GetDensityIndependentPixelRatio(Element* element) int ElementUtilities::GetStringWidth(Element* element, const String& string, Character prior_character) { const auto& computed = element->GetComputedValues(); - const TextShapingContext text_shaping_context{ computed.language(), computed.direction(), computed.letter_spacing() }; + const TextShapingContext text_shaping_context{computed.language(), computed.direction(), computed.letter_spacing()}; FontFaceHandle font_face_handle = element->GetFontFaceHandle(); if (font_face_handle == 0) @@ -198,7 +198,7 @@ bool ElementUtilities::GetClippingRegion(Element* element, Rectanglei& out_clip_ // Merge the existing clip region with the current clip region, unless we are ignoring clip regions. if (((clip_always || clip_enabled) && num_ignored_clips == 0) || force_clip_current_element) { - const BoxArea client_area = (force_clip_current_element ? BoxArea::Border : clipping_element->GetClientArea()); + const BoxArea clip_area = (force_clip_current_element ? BoxArea::Border : clipping_element->GetClipArea()); const bool has_clipping_content = (clip_always || force_clip_current_element || clipping_element->GetClientWidth() < clipping_element->GetScrollWidth() - 0.5f || clipping_element->GetClientHeight() < clipping_element->GetScrollHeight() - 0.5f); @@ -215,7 +215,7 @@ bool ElementUtilities::GetClippingRegion(Element* element, Rectanglei& out_clip_ // region to be clipped. If the element has a transform we only use a clip mask when the content clips. if (has_border_radius || (transform && has_clipping_content)) { - Geometry* clip_geometry = clipping_element->GetElementBackgroundBorder()->GetClipGeometry(clipping_element, client_area); + Geometry* clip_geometry = clipping_element->GetElementBackgroundBorder()->GetClipGeometry(clipping_element, clip_area); const ClipMaskOperation clip_operation = (out_clip_mask_list->empty() ? ClipMaskOperation::Set : ClipMaskOperation::Intersect); const Vector2f absolute_offset = clipping_element->GetAbsoluteOffset(BoxArea::Border); out_clip_mask_list->push_back(ClipMaskGeometry{clip_operation, clip_geometry, absolute_offset, transform}); @@ -231,8 +231,8 @@ bool ElementUtilities::GetClippingRegion(Element* element, Rectanglei& out_clip_ if (has_clipping_content && !disable_scissor_clipping) { // Shrink the scissor region to the element's client area. - Vector2f element_offset = clipping_element->GetAbsoluteOffset(client_area); - Vector2f element_size = clipping_element->GetBox().GetSize(client_area); + Vector2f element_offset = clipping_element->GetAbsoluteOffset(clip_area); + Vector2f element_size = clipping_element->GetBox().GetSize(clip_area); clip_region.IntersectIfValid(Rectanglef::FromPositionSize(element_offset, element_size)); } diff --git a/Source/Core/Elements/WidgetTextInput.cpp b/Source/Core/Elements/WidgetTextInput.cpp index 97286e459..28b832746 100644 --- a/Source/Core/Elements/WidgetTextInput.cpp +++ b/Source/Core/Elements/WidgetTextInput.cpp @@ -193,7 +193,6 @@ WidgetTextInput::WidgetTextInput(ElementFormControl* _parent) : parent->SetProperty(PropertyId::Drag, Property(Style::Drag::Drag)); parent->SetProperty(PropertyId::WordBreak, Property(Style::WordBreak::BreakWord)); parent->SetProperty(PropertyId::TextTransform, Property(Style::TextTransform::None)); - parent->SetClientArea(BoxArea::Content); parent->AddEventListener(EventId::Keydown, this, true); parent->AddEventListener(EventId::Textinput, this, true); @@ -1062,15 +1061,15 @@ int WidgetTextInput::CalculateLineIndex(float position) const float WidgetTextInput::GetAlignmentSpecificTextOffset(const char* p_begin, int line_index) const { - const float client_width = parent->GetClientWidth(); + const float available_width = GetAvailableWidth(); const float total_width = (float)ElementUtilities::GetStringWidth(text_element, String(p_begin, lines[line_index].editable_length)); auto text_align = GetElement()->GetComputedValues().text_align(); // offset position depending on text align switch (text_align) { - case Style::TextAlign::Right: return Math::Max(0.0f, (client_width - total_width)); - case Style::TextAlign::Center: return Math::Max(0.0f, ((client_width - total_width) / 2)); + case Style::TextAlign::Right: return Math::Max(0.0f, (available_width - total_width)); + case Style::TextAlign::Center: return Math::Max(0.0f, ((available_width - total_width) / 2)); default: break; } @@ -1125,13 +1124,13 @@ void WidgetTextInput::ShowCursor(bool show, bool move_to_cursor) // Shift the cursor into view. if (move_to_cursor) { - float minimum_scroll_top = (cursor_position.y + cursor_size.y) - parent->GetClientHeight(); + float minimum_scroll_top = (cursor_position.y + cursor_size.y) - GetAvailableHeight(); if (parent->GetScrollTop() < minimum_scroll_top) parent->SetScrollTop(minimum_scroll_top); else if (parent->GetScrollTop() > cursor_position.y) parent->SetScrollTop(cursor_position.y); - float minimum_scroll_left = (cursor_position.x + cursor_size.x) - parent->GetClientWidth(); + float minimum_scroll_left = (cursor_position.x + cursor_size.x) - GetAvailableWidth(); if (parent->GetScrollLeft() < minimum_scroll_left) parent->SetScrollLeft(minimum_scroll_left); else if (parent->GetScrollLeft() > cursor_position.x) @@ -1178,26 +1177,28 @@ void WidgetTextInput::FormatElement() scroll->DisableScrollbar(ElementScroll::VERTICAL); // If the formatting produces scrollbars we need to format again later, this constraint enables early exit for the first formatting round. - const float formatting_height_constraint = (y_overflow_property == Overflow::Auto ? parent->GetClientHeight() : FLT_MAX); + const float formatting_height_constraint = (y_overflow_property == Overflow::Auto ? GetAvailableHeight() : FLT_MAX); // Format the text and determine its total area. Vector2f content_area = FormatText(formatting_height_constraint); // If we're set to automatically generate horizontal scrollbars, check for that now. - if (!word_wrap && x_overflow_property == Overflow::Auto && content_area.x > parent->GetClientWidth() + OVERFLOW_TOLERANCE) + if (!word_wrap && x_overflow_property == Overflow::Auto && content_area.x > GetAvailableWidth() + OVERFLOW_TOLERANCE) scroll->EnableScrollbar(ElementScroll::HORIZONTAL, width); // Now check for vertical overflow. If we do turn on the scrollbar, this will cause a reflow. - if (y_overflow_property == Overflow::Auto && content_area.y > parent->GetClientHeight() + OVERFLOW_TOLERANCE) + if (y_overflow_property == Overflow::Auto && content_area.y > GetAvailableHeight() + OVERFLOW_TOLERANCE) { scroll->EnableScrollbar(ElementScroll::VERTICAL, width); content_area = FormatText(); - if (!word_wrap && x_overflow_property == Overflow::Auto && content_area.x > parent->GetClientWidth() + OVERFLOW_TOLERANCE) + if (!word_wrap && x_overflow_property == Overflow::Auto && content_area.x > GetAvailableWidth() + OVERFLOW_TOLERANCE) scroll->EnableScrollbar(ElementScroll::HORIZONTAL, width); } - parent->SetScrollableOverflowRectangle(content_area); + // For text elements, make the content and padding on all sides reachable by scrolling. + const Vector2f padding_size = parent->GetBox().GetFrameSize(BoxArea::Padding); + parent->SetScrollableOverflowRectangle(content_area + padding_size); scroll->FormatScrollbars(); } @@ -1225,7 +1226,7 @@ Vector2f WidgetTextInput::FormatText(float height_constraint) // When the selection contains endlines, we expand the selection area by this width. const int endline_font_width = int(0.4f * parent->GetComputedValues().font_size()); - const float client_width = parent->GetClientWidth(); + const float available_width = GetAvailableWidth(); int line_begin = 0; Vector2f line_position = {0, top_to_baseline}; bool last_line = false; @@ -1253,7 +1254,7 @@ Vector2f WidgetTextInput::FormatText(float height_constraint) // Keep generating lines until all the text content is placed. do { - if (client_width <= 0.f) + if (available_width <= 0.f) { lines.push_back(Line{}); break; @@ -1265,7 +1266,8 @@ Vector2f WidgetTextInput::FormatText(float height_constraint) String line_content; // Generate the next line. - last_line = text_element->GenerateLine(line_content, line.size, line_width, line_begin, client_width - cursor_size.x, 0, false, false, false); + last_line = + text_element->GenerateLine(line_content, line.size, line_width, line_begin, available_width - cursor_size.x, 0, false, false, false); // If this line terminates in a soft-return (word wrap), then the line may be leaving a space or two behind as an orphan. If so, we must // append the orphan onto the line even though it will push the line outside of the input field's bounds. @@ -1606,4 +1608,14 @@ void WidgetTextInput::SetKeyboardActive(bool active) } } +float WidgetTextInput::GetAvailableWidth() const +{ + return parent->GetClientWidth() - parent->GetBox().GetFrameSize(BoxArea::Padding).x; +} + +float WidgetTextInput::GetAvailableHeight() const +{ + return parent->GetClientHeight() - parent->GetBox().GetFrameSize(BoxArea::Padding).y; +} + } // namespace Rml diff --git a/Source/Core/Elements/WidgetTextInput.h b/Source/Core/Elements/WidgetTextInput.h index 957426c90..3c27f356e 100644 --- a/Source/Core/Elements/WidgetTextInput.h +++ b/Source/Core/Elements/WidgetTextInput.h @@ -224,6 +224,11 @@ class WidgetTextInput : public EventListener { /// @param[in] line_begin The absolute index at the beginning of the line. void GetLineIMEComposition(String& pre_composition, String& ime_composition, const String& line, int line_begin) const; + /// Returns the width available for the text contents without overflowing, that is, the content area subtracted by any scrollbar. + float GetAvailableWidth() const; + /// Returns the height available for the text contents without overflowing, that is, the content area subtracted by any scrollbar. + float GetAvailableHeight() const; + struct Line { // Offset into the text field's value. int value_offset; diff --git a/Source/Core/Elements/WidgetTextInputSingleLine.cpp b/Source/Core/Elements/WidgetTextInputSingleLine.cpp index 1451450b5..68caa142e 100644 --- a/Source/Core/Elements/WidgetTextInputSingleLine.cpp +++ b/Source/Core/Elements/WidgetTextInputSingleLine.cpp @@ -29,11 +29,16 @@ #include "WidgetTextInputSingleLine.h" #include "../../../Include/RmlUi/Core/Dictionary.h" #include "../../../Include/RmlUi/Core/ElementText.h" +#include "../../../Include/RmlUi/Core/Elements/ElementFormControl.h" #include namespace Rml { -WidgetTextInputSingleLine::WidgetTextInputSingleLine(ElementFormControl* parent) : WidgetTextInput(parent) {} +WidgetTextInputSingleLine::WidgetTextInputSingleLine(ElementFormControl* parent) : WidgetTextInput(parent) +{ + // Single line text controls should clip to the content area, see visual test: text_input_overflow.rml + parent->SetClipArea(BoxArea::Content); +} void WidgetTextInputSingleLine::SanitizeValue(String& value) {