Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle newlines in FormattedText #2165

Merged
merged 1 commit into from
Mar 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
212 changes: 184 additions & 28 deletions src/Eto.Gtk/Drawing/FormattedTextHandler.cs
Original file line number Diff line number Diff line change
@@ -1,47 +1,182 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Eto.Drawing;

namespace Eto.GtkSharp.Drawing
{
public class FormattedTextHandler : WidgetHandler<object, FormattedText, FormattedText.ICallback>, FormattedText.IHandler
{
public FormattedTextWrapMode Wrap { get; set; }
public FormattedTextTrimming Trimming { get; set; }
public string Text { get; set; }
public SizeF MaximumSize { get; set; } = SizeF.MaxValue;
public Font Font { get; set; }
public Brush ForegroundBrush { get; set; } = new SolidBrush(SystemColors.ControlText);
public FormattedTextAlignment Alignment { get; set; }
public int MaximumLineCount { get; set; }
FormattedTextWrapMode _wrap;
FormattedTextTrimming _trimming;
string _text;
SizeF _maximumSize = SizeF.MaxValue;
Font _font;
Brush _foregroundBrush = new SolidBrush(SystemColors.ControlText);
FormattedTextAlignment _alignment;
int _maximumLineCount;
bool _shouldClip;

public FormattedTextWrapMode Wrap
{
get => _wrap;
set
{
if (_wrap != value)
{
_wrap = value;
Invalidate();
}
}
}
public FormattedTextTrimming Trimming
{
get => _trimming;
set
{
if (_trimming != value)
{
_trimming = value;
Invalidate();
}
}
}
public string Text
{
get => _text;
set
{
if (_text != value)
{
_text = value;
Invalidate();
}
}
}

public SizeF MaximumSize
{
get => _maximumSize;
set
{
if (_maximumSize != value)
{
_maximumSize = value;
Invalidate();
}
}
}

public Font Font
{
get => _font;
set
{
if (_font != value)
{
_font = value;
Invalidate();
}
}
}

public Brush ForegroundBrush
{
get => _foregroundBrush;
set
{
if (_foregroundBrush != value)
{
_foregroundBrush = value;
Invalidate();
}
}
}

public FormattedTextAlignment Alignment
{
get => _alignment;
set
{
if (_alignment != value)
{
_alignment = value;
Invalidate();
}
}
}
public int MaximumLineCount
{
get => _maximumLineCount;
set
{
if (_maximumLineCount != value)
{
_maximumLineCount = value;
Invalidate();
}
}
}

void Invalidate()
{
_layout?.Dispose();
_layout = null;
}

public SizeF Measure()
{
// can we do this more lightweight than creating a control?
using (var ctl = new Gtk.Label())
using (var layout = new Pango.Layout(ctl.PangoContext))
if (_layout == null)
{
Setup(layout);
layout.GetPixelSize(out var width, out var height);
return new SizeF(width, height);
EnsureLayout(new Gtk.Label().PangoContext);
}
_layout.GetPixelSize(out var width, out var height);
var size = new SizeF(width, height);
if (Wrap == FormattedTextWrapMode.None && IsUnlimited(MaximumSize.Width))
size.Width = Math.Min(MaximumSize.Width, width);
return size;
}

const int MaxLayoutSize = 1000000;

void Setup(Pango.Layout layout)
{
Font.Apply(layout);
layout.Width = (int)(MaximumSize.Width * Pango.Scale.PangoScale);
var hasNewlines = Text.IndexOf('\n') != -1;
_shouldClip = false;
var isRightOrCenter = Alignment == FormattedTextAlignment.Right || Alignment == FormattedTextAlignment.Center;
var size = SizeF.Min(MaximumSize, new SizeF(MaxLayoutSize, MaxLayoutSize));
if (size.Width <= 0)
size.Width = MaxLayoutSize;
if (size.Height <= 0)
size.Height = MaxLayoutSize;

layout.Width = (int)(size.Width * Pango.Scale.PangoScale);

#if GTK3
layout.Height = (int)(MaximumSize.Height * Pango.Scale.PangoScale);
layout.Height = (int)(size.Height * Pango.Scale.PangoScale);
#endif
layout.Ellipsize = Trimming == FormattedTextTrimming.None ? Pango.EllipsizeMode.None : Pango.EllipsizeMode.End;
switch (Wrap)
{
case FormattedTextWrapMode.None:
// only draw one line!!
layout.Wrap = Pango.WrapMode.Char;

if (Trimming == FormattedTextTrimming.None || hasNewlines || (isRightOrCenter && !hasNewlines))
{
_shouldClip = true;
layout.Width = (int)(MaxLayoutSize * Pango.Scale.PangoScale);
}
// if (_shouldClip)
// layout.Width = (int)(MaxLayoutSize * Pango.Scale.PangoScale);
#if GTK3
layout.Height = (int)((double)layout.FontDescription.Size / (double)Pango.Scale.PangoScale);
// only draw a single line so we can do ellipsizing
if (!hasNewlines)
layout.Height = layout.FontDescription.Size;
#endif

break;
case FormattedTextWrapMode.Word:
layout.Wrap = Pango.WrapMode.Word;
Expand All @@ -67,16 +202,18 @@ void Setup(Pango.Layout layout)
break;
}
layout.SetText(Text);
if (Wrap == FormattedTextWrapMode.None && layout.LineCount > 1)
if ((layout.Width >= MaxLayoutSize || !hasNewlines) && isRightOrCenter)
{
// line includes the full last word so keep shrinking until it isn't wrapped
var len = layout.GetLine(0).Length;
while (len > 0 && layout.IsWrapped)
{
layout.SetText(Text.Substring(0, len--));
}
layout.GetPixelSize(out var width, out var height);
if (hasNewlines)
layout.Width = (int)(width * Pango.Scale.PangoScale);
else if (IsUnlimited(MaximumSize.Width))
layout.Width = (int)(Math.Max(width, MaximumSize.Width) * Pango.Scale.PangoScale);
else
layout.Width = (int)(Math.Min(width, MaximumSize.Width) * Pango.Scale.PangoScale);

}
if (Trimming == FormattedTextTrimming.None && layout.LineCount > 1)
if (Trimming == FormattedTextTrimming.None && layout.LineCount > 1 && IsUnlimited(MaximumSize.Height))
{
layout.GetPixelSize(out _, out var height);
while (layout.LineCount > 1 && height > MaximumSize.Height)
Expand Down Expand Up @@ -106,15 +243,34 @@ void Setup(Pango.Layout layout)
}
}

public void Draw(GraphicsHandler graphics, Pango.Layout layout, Cairo.Context context, PointF location)
Pango.Layout _layout;

bool IsUnlimited(float value) => value < float.MaxValue || value <= 0;

public void Draw(GraphicsHandler graphics, Cairo.Context context, PointF location)
{
Setup(layout);
EnsureLayout(graphics.PangoContext);
context.Save();
if (_shouldClip && IsUnlimited(MaximumSize.Width))
{
context.Rectangle(new Cairo.Rectangle(location.X, location.Y, Math.Min(MaxLayoutSize, MaximumSize.Width), Math.Min(MaxLayoutSize, MaximumSize.Height)));
context.Clip();
}
ForegroundBrush.Apply(graphics);
context.MoveTo(location.X, location.Y);
Pango.CairoHelper.LayoutPath(context, layout);
Pango.CairoHelper.LayoutPath(context, _layout);
context.Fill();
context.Restore();
}

private void EnsureLayout(Pango.Context context)
{
if (_layout == null || _layout.Context.Handle != context.Handle)
{
_layout?.Dispose();
_layout = new Pango.Layout(context);
Setup(_layout);
}
}
}
}
7 changes: 2 additions & 5 deletions src/Eto.Gtk/Drawing/GraphicsHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -403,11 +403,8 @@ public void DrawText(FormattedText formattedText, PointF location)
var oldAA = AntiAlias;
AntiAlias = true;
SetOffset(false);
using (var layout = CreateLayout())
{
var handler = (FormattedTextHandler)formattedText.Handler;
handler.Draw(this, layout, Control, location);
}
var handler = (FormattedTextHandler)formattedText.Handler;
handler.Draw(this, Control, location);
AntiAlias = oldAA;
}

Expand Down
70 changes: 49 additions & 21 deletions src/Eto.Mac/Drawing/FormattedTextHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ public class FormattedTextHandler : WidgetHandler<EtoLayoutManager, FormattedTex
FormattedTextTrimming _trimming;
FormattedTextWrapMode _wrap;
FormattedTextAlignment _alignment;
SizeF _maximumSize = SizeF.MaxValue;

public FormattedTextAlignment Alignment
{
Expand Down Expand Up @@ -148,8 +149,47 @@ public string Text

public SizeF MaximumSize
{
get => container.ContainerSize.ToEto();
set => container.ContainerSize = value.ToNS();
get => _maximumSize;
set
{
_maximumSize = value;
SetMaxSize();
}
}

private void SetMaxSize()
{
var size = _maximumSize;
if (size.Width >= float.MaxValue && Alignment != FormattedTextAlignment.Left)
{
// need a width to support aligning
size.Width = GetMaxTextWidth();
}
size.Width = Math.Min(int.MaxValue, size.Width);
size.Height = Math.Min(int.MaxValue, size.Height);
container.Size = size.ToNS();
}

private float GetMaxTextWidth()
{
float maxWidth = 0;
char newline = '\n';
int newlineIndex = _text.IndexOf(newline);
if (newlineIndex == -1)
{
return _maximumSize.Width;
}
int startIndex = 0;
container.Size = new CGSize(0, 0);
while (newlineIndex >= 0)
{
var glyphRange = new NSRange(startIndex, newlineIndex - startIndex);
var rect = Control.BoundingRectForGlyphRange(glyphRange, container).Size.ToEto();
maxWidth = Math.Max(maxWidth, rect.Width);
startIndex = newlineIndex + 1;
newlineIndex = _text.IndexOf(newline, startIndex);
}
return maxWidth;
}

public Font Font
Expand Down Expand Up @@ -192,19 +232,8 @@ private NSAttributedString CreateAttributedString()
private NSParagraphStyle CreateParagraphStyle()
{
var style = new NSMutableParagraphStyle();
//style.LineBreakMode = Trimming.ToNS();
container.MaximumNumberOfLines = 0;
if (Wrap == FormattedTextWrapMode.None)
{
if (Trimming != FormattedTextTrimming.None)
style.LineBreakMode = Trimming.ToNS();
else
{
// hm, setting style.LineBreakMode to Clipping doesn't appear to clip, so we wrap by character and set max lines to 1
style.LineBreakMode = NSLineBreakMode.CharWrapping;
container.MaximumNumberOfLines = 1;
}
}
style.LineBreakMode = Trimming.ToNS();
else
style.LineBreakMode = Wrap.ToNS();

Expand All @@ -217,6 +246,7 @@ void EnsureString()
if (invalid)
{
storage.SetString(CreateAttributedString());
SetMaxSize();
invalid = false;
}
}
Expand All @@ -230,11 +260,8 @@ public int MaximumLineCount
public SizeF Measure()
{
EnsureString();
var size = Control.BoundingRectForGlyphRange(new NSRange(0, (int)_text.Length), container).Size.ToEto();
/*if (Wrap == FormattedTextWrapMode.None && Trimming != FormattedTextTrimming.None && Alignment != FormattedTextAlignment.Left)
{
size.Width = MaximumSize.Width;
}*/
// var size = Control.BoundingRectForGlyphRange(new NSRange(0, (int)_text.Length), container).Size.ToEto();
var size = storage.BoundingRectWithSize(container.Size, NSStringDrawingOptions.UsesLineFragmentOrigin).Size.ToEto();
return size;
}

Expand All @@ -245,7 +272,7 @@ public FormattedTextHandler()
#if OSX
Control.BackgroundLayoutEnabled = false;
#endif
container = new NSTextContainer { LineFragmentPadding = 0f };
container = new NSTextContainer { LineFragmentPadding = 0f, Size = new CGSize(int.MaxValue, int.MaxValue) };
Control.AddTextContainer(container);
storage.AddLayoutManager(Control);
}
Expand All @@ -255,7 +282,8 @@ public void DrawText(GraphicsHandler graphics, PointF location)
EnsureString();
Control.CurrentGraphics = graphics;
var ctx = graphics.Control;
Control.DrawGlyphs(new NSRange(0, (int)_text.Length), location.ToNS());
storage.DrawString(new CGRect(location.ToNS(), container.Size), NSStringDrawingOptions.UsesLineFragmentOrigin | NSStringDrawingOptions.TruncatesLastVisibleLine);
// Control.DrawGlyphs(new NSRange(0, (int)_text.Length), location.ToNS());
}
}
}
Loading