Skip to content

Commit

Permalink
Merge pull request picoe#2165 from cwensley/curtis/formattedtext-newl…
Browse files Browse the repository at this point in the history
…ines

Handle newlines in FormattedText
  • Loading branch information
cwensley authored Mar 18, 2022
2 parents efc7024 + 9fed84e commit cd6bf51
Show file tree
Hide file tree
Showing 13 changed files with 455 additions and 116 deletions.
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

0 comments on commit cd6bf51

Please sign in to comment.