mirror of
https://github.com/slint-ui/slint.git
synced 2025-08-04 18:58:36 +00:00
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:
parent
eb60c26398
commit
9f63d157d1
13 changed files with 66 additions and 28 deletions
|
@ -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 _)
|
||||
}
|
||||
|
|
|
@ -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.)
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue