Add character wrapping for Qt Backend

This adds a new wrapping mode called `char-wrap`, which allows for wrapping at any character.
Currently, it only supports the Qt backend, with the other backends falling back to `word-wrap` when this option is selected.
This commit is contained in:
Daniel Stuart 2024-06-09 15:40:00 -03:00 committed by Simon Hausmann
parent eb60c26398
commit 9f63d157d1
13 changed files with 66 additions and 28 deletions

View file

@ -358,7 +358,7 @@ cpp! {{
// if line_for_y_pos > 0, then the function will return the line at this y position
static int do_text_layout(QTextLayout &layout, int flags, const QRectF &rect, int line_for_y_pos = -1) {
QTextOption options;
options.setWrapMode((flags & Qt::TextWordWrap) ? QTextOption::WordWrap : QTextOption::NoWrap);
options.setWrapMode((flags & Qt::TextWordWrap) ? QTextOption::WordWrap : ((flags & Qt::TextWrapAnywhere) ? QTextOption::WrapAnywhere : QTextOption::NoWrap));
if (flags & Qt::AlignHCenter)
options.setAlignment(Qt::AlignCenter);
else if (flags & Qt::AlignLeft)
@ -676,7 +676,8 @@ impl ItemRenderer for QtItemRenderer<'_> {
TextVerticalAlignment::Center => key_generated::Qt_AlignmentFlag_AlignVCenter,
TextVerticalAlignment::Bottom => key_generated::Qt_AlignmentFlag_AlignBottom,
};
let wrap = text.wrap() == TextWrap::WordWrap;
let wrap = text.wrap() != TextWrap::NoWrap;
let word_wrap = text.wrap() == TextWrap::WordWrap;
let elide = text.overflow() == TextOverflow::Elide;
let stroke_visible = !text.stroke().is_transparent();
let stroke_outside = text.stroke_style() == TextStrokeStyle::Outside;
@ -685,7 +686,7 @@ impl ItemRenderer for QtItemRenderer<'_> {
TextStrokeStyle::Center => text.stroke_width().get(),
};
let painter: &mut QPainterPtr = &mut self.painter;
cpp! { unsafe [painter as "QPainterPtr*", rect as "QRectF", fill_brush as "QBrush", stroke_brush as "QBrush", mut string as "QString", font as "QFont", elide as "bool", alignment as "Qt::Alignment", wrap as "bool", stroke_visible as "bool", stroke_outside as "bool", stroke_width as "float"] {
cpp! { unsafe [painter as "QPainterPtr*", rect as "QRectF", fill_brush as "QBrush", stroke_brush as "QBrush", mut string as "QString", font as "QFont", elide as "bool", alignment as "Qt::Alignment", wrap as "bool", word_wrap as "bool", stroke_visible as "bool", stroke_outside as "bool", stroke_width as "float"] {
QString elided;
if (!elide) {
elided = string;
@ -709,7 +710,11 @@ impl ItemRenderer for QtItemRenderer<'_> {
QFontMetrics fm(font);
QTextLayout layout(string, font);
QTextOption options;
options.setWrapMode(QTextOption::WordWrap);
if (word_wrap) {
options.setWrapMode(QTextOption::WordWrap);
} else {
options.setWrapMode(QTextOption::WrapAnywhere);
}
layout.setTextOption(options);
layout.setCacheEnabled(true);
layout.beginLayout();
@ -739,8 +744,13 @@ impl ItemRenderer for QtItemRenderer<'_> {
if (!stroke_visible) {
int flags = alignment;
if (wrap)
flags |= Qt::TextWordWrap;
if (wrap) {
if (word_wrap) {
flags |= Qt::TextWordWrap;
} else {
flags |= Qt::TextWrapAnywhere;
}
}
(*painter)->setFont(font);
(*painter)->setBrush(Qt::NoBrush);
@ -754,8 +764,13 @@ impl ItemRenderer for QtItemRenderer<'_> {
QTextOption options = document.defaultTextOption();
options.setAlignment(alignment);
if (wrap)
options.setWrapMode(QTextOption::WordWrap);
if (wrap) {
if (word_wrap) {
options.setWrapMode(QTextOption::WordWrap);
} else {
options.setWrapMode(QTextOption::WrapAnywhere);
}
}
document.setDefaultTextOption(options);
// Workaround for https://bugreports.qt.io/browse/QTBUG-13467
@ -837,6 +852,7 @@ impl ItemRenderer for QtItemRenderer<'_> {
} | match text_input.wrap() {
TextWrap::NoWrap => 0,
TextWrap::WordWrap => key_generated::Qt_TextFlag_TextWordWrap,
TextWrap::CharWrap => key_generated::Qt_TextFlag_TextWrapAnywhere,
};
let visual_representation = text_input.visual_representation(Some(qt_password_character));
@ -2126,8 +2142,13 @@ impl i_slint_core::renderer::RendererSealed for QtWindow {
text: &str,
max_width: Option<LogicalLength>,
_scale_factor: ScaleFactor,
wrap_anywhere: bool,
) -> LogicalSize {
get_font(font_request).text_size(text, max_width.map(|logical_width| logical_width.get()))
get_font(font_request).text_size(
text,
max_width.map(|logical_width| logical_width.get()),
wrap_anywhere,
)
}
fn text_input_byte_offset_for_position(
@ -2160,6 +2181,7 @@ impl i_slint_core::renderer::RendererSealed for QtWindow {
} | match text_input.wrap() {
TextWrap::NoWrap => 0,
TextWrap::WordWrap => key_generated::Qt_TextFlag_TextWordWrap,
TextWrap::CharWrap => key_generated::Qt_TextFlag_TextWrapAnywhere,
};
let single_line: bool = text_input.single_line();
let byte_offset = cpp! { unsafe [font as "QFont", string as "QString", pos as "QPointF", flags as "int",
@ -2217,6 +2239,7 @@ impl i_slint_core::renderer::RendererSealed for QtWindow {
} | match text_input.wrap() {
TextWrap::NoWrap => 0,
TextWrap::WordWrap => key_generated::Qt_TextFlag_TextWordWrap,
TextWrap::CharWrap => key_generated::Qt_TextFlag_TextWrapAnywhere,
};
let single_line: bool = text_input.single_line();
let r = cpp! { unsafe [font as "QFont", mut string as "QString", offset as "int", flags as "int", rect as "QRectF", single_line as "bool"]
@ -2346,16 +2369,16 @@ fn get_font(request: FontRequest) -> QFont {
cpp_class! {pub unsafe struct QFont as "QFont"}
impl QFont {
fn text_size(&self, text: &str, max_width: Option<f32>) -> LogicalSize {
fn text_size(&self, text: &str, max_width: Option<f32>, wrap_anywhere: bool) -> LogicalSize {
let string = qttypes::QString::from(text);
let mut r = qttypes::QRectF::default();
if let Some(max) = max_width {
r.height = f32::MAX as _;
r.width = max as _;
}
let size = cpp! { unsafe [self as "const QFont*", string as "QString", r as "QRectF"]
let size = cpp! { unsafe [self as "const QFont*", string as "QString", r as "QRectF", wrap_anywhere as "bool"]
-> qttypes::QSizeF as "QSizeF"{
return QFontMetricsF(*self).boundingRect(r, r.isEmpty() ? 0 : Qt::TextWordWrap , string).size();
return QFontMetricsF(*self).boundingRect(r, r.isEmpty() ? 0 : ((wrap_anywhere) ? Qt::TextWrapAnywhere : Qt::TextWordWrap) , string).size();
}};
LogicalSize::new(size.width as _, size.height as _)
}

View file

@ -150,6 +150,7 @@ impl RendererSealed for TestingWindow {
text: &str,
_max_width: Option<LogicalLength>,
_scale_factor: ScaleFactor,
_wrap_anywhere: bool,
) -> LogicalSize {
LogicalSize::new(text.len() as f32 * 10., 10.)
}

View file

@ -46,6 +46,8 @@ macro_rules! for_each_enums {
NoWrap,
/// The text will be wrapped at word boundaries.
WordWrap,
/// The text will be wrapped at any character.
CharWrap,
}
/// This enum describes the how the text appear if it is too wide to fit in the [`Text`](elements.md#text) width.

View file

@ -61,7 +61,7 @@ export component TextEditBase inherits Rectangle {
y: root.scroll-view-padding;
width: parent.width - 2 * root.scroll-view-padding;
height: parent.height - 2 * root.scroll-view-padding;
viewport-width: root.wrap == TextWrap.word-wrap ? self.visible-width : max(self.visible-width, text-input.preferred-width);
viewport-width: root.wrap == TextWrap.no-wrap ? max(self.visible-width, text-input.preferred-width) : self.visible-width;
viewport-height: max(self.visible-height, text-input.preferred-height);
text-input := TextInput {

View file

@ -138,7 +138,7 @@ export component TextEdit {
y: 8px;
width: parent.width - 16px;
height: parent.height - 16px;
viewport-width: root.wrap == TextWrap.word-wrap ? self.visible-width : max(self.visible-width, text-input.preferred-width);
viewport-width: root.wrap == TextWrap.no-wrap ? max(self.visible-width, text-input.preferred-width) : self.visible-width;
viewport-height: max(self.visible-height, text-input.preferred-height);
text-input := TextInput {

View file

@ -68,12 +68,13 @@ impl Item for Text {
window_adapter: &Rc<dyn WindowAdapter>,
) -> LayoutInfo {
let window_inner = WindowInner::from_pub(window_adapter.window());
let implicit_size = |max_width| {
let implicit_size = |max_width, wrap_anywhere| {
window_adapter.renderer().text_size(
self.font_request(window_inner),
self.text().as_str(),
max_width,
ScaleFactor::new(window_adapter.window().scale_factor()),
wrap_anywhere,
)
};
@ -82,7 +83,7 @@ impl Item for Text {
// letters will be cut off, apply the ceiling here.
match orientation {
Orientation::Horizontal => {
let implicit_size = implicit_size(None);
let implicit_size = implicit_size(None, false);
let min = match self.overflow() {
TextOverflow::Elide => implicit_size.width.min(
window_adapter
@ -92,12 +93,13 @@ impl Item for Text {
"",
None,
ScaleFactor::new(window_inner.scale_factor()),
false,
)
.width,
),
TextOverflow::Clip => match self.wrap() {
TextWrap::NoWrap => implicit_size.width,
TextWrap::WordWrap => 0 as Coord,
TextWrap::WordWrap | TextWrap::CharWrap => 0 as Coord,
},
};
LayoutInfo {
@ -108,8 +110,9 @@ impl Item for Text {
}
Orientation::Vertical => {
let h = match self.wrap() {
TextWrap::NoWrap => implicit_size(None).height,
TextWrap::WordWrap => implicit_size(Some(self.width())).height,
TextWrap::NoWrap => implicit_size(None, false).height,
TextWrap::WordWrap => implicit_size(Some(self.width()), false).height,
TextWrap::CharWrap => implicit_size(Some(self.width()), true).height,
}
.ceil();
LayoutInfo { min: h, preferred: h, ..LayoutInfo::default() }
@ -297,7 +300,7 @@ impl Item for TextInput {
window_adapter: &Rc<dyn WindowAdapter>,
) -> LayoutInfo {
let text = self.text();
let implicit_size = |max_width| {
let implicit_size = |max_width, wrap_anywhere| {
window_adapter.renderer().text_size(
self.font_request(window_adapter),
{
@ -309,6 +312,7 @@ impl Item for TextInput {
},
max_width,
ScaleFactor::new(window_adapter.window().scale_factor()),
wrap_anywhere,
)
};
@ -317,10 +321,10 @@ impl Item for TextInput {
// letters will be cut off, apply the ceiling here.
match orientation {
Orientation::Horizontal => {
let implicit_size = implicit_size(None);
let implicit_size = implicit_size(None, false);
let min = match self.wrap() {
TextWrap::NoWrap => implicit_size.width,
TextWrap::WordWrap => 0 as Coord,
TextWrap::WordWrap | TextWrap::CharWrap => 0 as Coord,
};
LayoutInfo {
min: min.ceil(),
@ -330,8 +334,9 @@ impl Item for TextInput {
}
Orientation::Vertical => {
let h = match self.wrap() {
TextWrap::NoWrap => implicit_size(None).height,
TextWrap::WordWrap => implicit_size(Some(self.width())).height,
TextWrap::NoWrap => implicit_size(None, false).height,
TextWrap::WordWrap => implicit_size(Some(self.width()), false).height,
TextWrap::CharWrap => implicit_size(Some(self.width()), true).height,
}
.ceil();
LayoutInfo { min: h, preferred: h, ..LayoutInfo::default() }
@ -917,6 +922,7 @@ impl TextInput {
" ",
None,
ScaleFactor::new(window_adapter.window().scale_factor()),
false,
)
.height;

View file

@ -25,13 +25,16 @@ impl<T: RendererSealed> Renderer for T {}
/// users to re-implement these functions.
pub trait RendererSealed {
/// Returns the size of the given text in logical pixels.
/// When set, `max_width` means that one need to wrap the text so it does not go further than that
/// When set, `max_width` means that one need to wrap the text, so it does not go further than that.
/// When set, `wrap_anywhere` means that the text wrapping will occur at any given character, instead of
/// only at word boundaries.
fn text_size(
&self,
font_request: crate::graphics::FontRequest,
text: &str,
max_width: Option<LogicalLength>,
scale_factor: ScaleFactor,
wrap_anywhere: bool,
) -> LogicalSize;
/// Returns the (UTF-8) byte offset in the text property that refers to the character that contributed to

View file

@ -615,6 +615,7 @@ impl RendererSealed for SoftwareRenderer {
text: &str,
max_width: Option<LogicalLength>,
scale_factor: ScaleFactor,
_wrap_anywhere: bool, //TODO: Add support for char-wrap
) -> LogicalSize {
fonts::text_size(font_request, text, max_width, scale_factor)
}

View file

@ -114,7 +114,7 @@ impl<'a, Font: AbstractFont> TextParagraphLayout<'a, Font> {
) -> core::ops::ControlFlow<R>,
selection: Option<core::ops::Range<usize>>,
) -> Result<Font::Length, R> {
let wrap = self.wrap == TextWrap::WordWrap;
let wrap = self.wrap != TextWrap::NoWrap;
let elide = self.overflow == TextOverflow::Elide;
let elide_glyph = if elide {
self.layout.font.glyph_for_char('…').filter(|glyph| glyph.glyph_id.is_some())

View file

@ -564,7 +564,7 @@ pub(crate) fn layout_text_lines(
paint: &femtovg::Paint,
mut layout_line: impl FnMut(&str, PhysicalPoint, usize, &femtovg::TextMetrics),
) -> PhysicalLength {
let wrap = wrap == TextWrap::WordWrap;
let wrap = wrap != TextWrap::NoWrap;
let elide = overflow == TextOverflow::Elide;
let max_width = max_size.width_length();

View file

@ -365,6 +365,7 @@ impl RendererSealed for FemtoVGRenderer {
text: &str,
max_width: Option<LogicalLength>,
scale_factor: ScaleFactor,
_wrap_anywhere: bool, //TODO: Add support for char-wrap
) -> LogicalSize {
crate::fonts::text_size(&font_request, scale_factor, text, max_width)
}

View file

@ -355,6 +355,7 @@ impl i_slint_core::renderer::RendererSealed for SkiaRenderer {
text: &str,
max_width: Option<LogicalLength>,
scale_factor: ScaleFactor,
_wrap_anywhere: bool, //TODO: Add support for char-wrap
) -> LogicalSize {
let (layout, _) = textlayout::create_layout(
font_request,

View file

@ -89,7 +89,7 @@ pub fn create_layout(
if overflow == items::TextOverflow::Elide {
style.set_ellipsis("");
if wrap == items::TextWrap::WordWrap {
if wrap != items::TextWrap::NoWrap {
let metrics = text_style.font_metrics();
let line_height = metrics.descent - metrics.ascent + metrics.leading;
style.set_max_lines((max_height.get() / line_height).floor() as usize);