Skip to content

Commit ba30d02

Browse files
Update to latest Fonts and add APIs, tests and samples
1 parent 466e4a2 commit ba30d02

22 files changed

Lines changed: 1871 additions & 88 deletions

samples/DrawShapesWithImageSharp/Program.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -789,7 +789,6 @@ private static void DrawTypographySheet()
789789
const string pathText = "Path text follows the curve while runs vary colour, weight, and decoration.";
790790
RichTextOptions pathOptions = new(pathFont)
791791
{
792-
Path = textPath,
793792
WrappingLength = textPath.ComputeLength(),
794793
HorizontalAlignment = HorizontalAlignment.Left,
795794
VerticalAlignment = VerticalAlignment.Bottom,
@@ -832,15 +831,15 @@ private static void DrawTypographySheet()
832831
]
833832
};
834833

835-
// Setting RichTextOptions.Path tells the layout to advance each glyph along the curve
834+
// Passing a path to DrawText tells the renderer to advance each glyph along the curve
836835
// rather than along a straight baseline. WrappingLength = textPath.ComputeLength()
837836
// matches the wrap budget to the curve's arc length, so the text fills the path
838837
// without overflowing past its end. The guide path can be drawn as well as used as
839838
// a baseline — that is a design choice. Strokes, ribbons, dashed leaders, or a
840839
// decorative trail under the glyphs all work, and so does omitting the stroke and
841840
// rendering only the text. The two operations are independent.
842841
canvas.Draw(Pens.Solid(ruleColor, 1.5F), textPath);
843-
canvas.DrawText(pathOptions, pathText, inkBrush, null);
842+
canvas.DrawText(pathOptions, pathText, textPath, inkBrush, null);
844843
});
845844

846845
static void DrawPanel(

samples/WebGPUExternalSurfaceDemo/MainForm.cs

Lines changed: 236 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
// Licensed under the Six Labors Split License.
33

44
using SixLabors.ImageSharp.Drawing.Processing;
5-
using SixLabors.ImageSharp.PixelFormats;
65
using WebGPUExternalSurfaceDemo.Controls;
76
using WebGPUExternalSurfaceDemo.Scenes;
7+
using ImageSharpColor = SixLabors.ImageSharp.Color;
88

99
namespace WebGPUExternalSurfaceDemo;
1010

@@ -18,10 +18,21 @@ internal sealed class MainForm : Form
1818
private readonly WebGPURenderControl clockControl;
1919
private readonly WebGPURenderControl tigerControl;
2020
private readonly WebGPURenderControl applyControl;
21+
private readonly WebGPURenderControl manualTextFlowControl;
22+
private readonly WebGPURenderControl richTextControl;
2123
private readonly ClockScene clockScene;
2224
private readonly TigerViewerScene tigerScene;
2325
private readonly ApplyReadbackScene applyScene;
26+
private readonly ManualTextFlowScene manualTextFlowScene;
27+
private readonly RichTextEditorScene richTextScene;
2428
private readonly Label tigerStatusLabel;
29+
private readonly CheckBox boldButton;
30+
private readonly CheckBox italicButton;
31+
private readonly CheckBox underlineButton;
32+
private readonly CheckBox strikeoutButton;
33+
private readonly ComboBox fontFamilyComboBox;
34+
private readonly Label fontSizeLabel;
35+
private readonly Label selectionStatusLabel;
2536

2637
public MainForm()
2738
{
@@ -33,6 +44,8 @@ public MainForm()
3344
this.clockScene = new ClockScene();
3445
this.tigerScene = new TigerViewerScene();
3546
this.applyScene = new ApplyReadbackScene();
47+
this.manualTextFlowScene = new ManualTextFlowScene();
48+
this.richTextScene = new RichTextEditorScene();
3649

3750
// Each tab gets its own render control and external surface. This mirrors real UI applications where
3851
// separate controls or panels own their own native drawable areas.
@@ -45,6 +58,12 @@ public MainForm()
4558
this.applyControl = new WebGPURenderControl { Dock = DockStyle.Fill };
4659
this.applyControl.PaintFrame += this.OnPaintApply;
4760

61+
this.manualTextFlowControl = new WebGPURenderControl { Dock = DockStyle.Fill };
62+
this.manualTextFlowControl.PaintFrame += this.OnPaintManualTextFlow;
63+
64+
this.richTextControl = new WebGPURenderControl { Dock = DockStyle.Fill, TabStop = true };
65+
this.richTextControl.PaintFrame += this.OnPaintRichText;
66+
4867
// The Apply scene reacts to pointer movement so readback cost can be assessed interactively.
4968
this.applyControl.MouseDown += (_, e) =>
5069
{
@@ -75,6 +94,69 @@ public MainForm()
7594
};
7695
this.tigerControl.Controls.Add(this.tigerStatusLabel);
7796

97+
this.boldButton = this.CreateEditorToggleButton("B", () => this.richTextScene.ToggleBold());
98+
this.italicButton = this.CreateEditorToggleButton("I", () => this.richTextScene.ToggleItalic());
99+
this.underlineButton = this.CreateEditorToggleButton("U", () => this.richTextScene.ToggleUnderline());
100+
this.strikeoutButton = this.CreateEditorToggleButton("S", () => this.richTextScene.ToggleStrikeout());
101+
this.fontFamilyComboBox = new ComboBox
102+
{
103+
DropDownStyle = ComboBoxStyle.DropDownList,
104+
Width = 240,
105+
Margin = new Padding(0, 0, 8, 0),
106+
};
107+
108+
foreach (string name in SixLabors.Fonts.SystemFonts.Collection.Families.Select(x => x.Name).Order())
109+
{
110+
this.fontFamilyComboBox.Items.Add(name);
111+
}
112+
113+
this.fontFamilyComboBox.SelectedItem = this.richTextScene.FontFamilyName;
114+
this.fontFamilyComboBox.SelectedIndexChanged += (_, _) =>
115+
{
116+
if (this.fontFamilyComboBox.SelectedItem is string name)
117+
{
118+
this.richTextScene.SetFontFamily(name);
119+
this.richTextControl.Focus();
120+
this.richTextControl.Invalidate();
121+
}
122+
};
123+
124+
this.selectionStatusLabel = new Label
125+
{
126+
AutoSize = true,
127+
Margin = new Padding(10, 5, 0, 0),
128+
};
129+
this.fontSizeLabel = new Label
130+
{
131+
AutoSize = true,
132+
Margin = new Padding(0, 5, 8, 0),
133+
};
134+
135+
FlowLayoutPanel editorToolbar = new()
136+
{
137+
Dock = DockStyle.Top,
138+
AutoSize = true,
139+
Padding = new Padding(8),
140+
};
141+
142+
editorToolbar.Controls.Add(this.fontFamilyComboBox);
143+
editorToolbar.Controls.Add(this.boldButton);
144+
editorToolbar.Controls.Add(this.italicButton);
145+
editorToolbar.Controls.Add(this.underlineButton);
146+
editorToolbar.Controls.Add(this.strikeoutButton);
147+
editorToolbar.Controls.Add(this.CreateEditorButton("A-", () => this.richTextScene.ChangeFontSize(-2F)));
148+
editorToolbar.Controls.Add(this.fontSizeLabel);
149+
editorToolbar.Controls.Add(this.CreateEditorButton("A+", () => this.richTextScene.ChangeFontSize(2F)));
150+
editorToolbar.Controls.Add(this.CreateEditorColorButton(Color.Black, ImageSharpColor.ParseHex("#17212B")));
151+
editorToolbar.Controls.Add(this.CreateEditorColorButton(Color.RoyalBlue, ImageSharpColor.ParseHex("#145DA0")));
152+
editorToolbar.Controls.Add(this.CreateEditorColorButton(Color.Firebrick, ImageSharpColor.ParseHex("#B33A3A")));
153+
editorToolbar.Controls.Add(this.CreateEditorColorButton(Color.SeaGreen, ImageSharpColor.ParseHex("#2B7A4B")));
154+
editorToolbar.Controls.Add(this.selectionStatusLabel);
155+
156+
Panel richTextPanel = new() { Dock = DockStyle.Fill };
157+
richTextPanel.Controls.Add(this.richTextControl);
158+
richTextPanel.Controls.Add(editorToolbar);
159+
78160
// Mouse input stays in WinForms coordinates. The scene converts it into its own world transform.
79161
this.tigerControl.MouseDown += (_, e) =>
80162
{
@@ -99,6 +181,59 @@ public MainForm()
99181
this.tigerControl.Invalidate();
100182
};
101183

184+
// The manual text-flow scene keeps prepared text static and lets the mouse
185+
// move only the obstacle that determines each per-line wrapping width.
186+
this.manualTextFlowControl.MouseDown += (_, e) =>
187+
{
188+
this.manualTextFlowScene.OnMouseDown(e);
189+
this.manualTextFlowControl.Invalidate();
190+
};
191+
this.manualTextFlowControl.MouseMove += (_, e) =>
192+
{
193+
this.manualTextFlowScene.OnMouseMove(e);
194+
this.manualTextFlowControl.Invalidate();
195+
};
196+
197+
this.richTextControl.MouseDown += (_, e) =>
198+
{
199+
this.richTextControl.Focus();
200+
if (e.Button == MouseButtons.Left)
201+
{
202+
this.richTextControl.Capture = true;
203+
}
204+
205+
this.richTextScene.OnMouseDown(e);
206+
this.UpdateEditorToolbar();
207+
this.richTextControl.Invalidate();
208+
};
209+
210+
this.richTextControl.MouseMove += (_, e) =>
211+
{
212+
this.richTextScene.OnMouseMove(e);
213+
this.UpdateEditorToolbar();
214+
this.richTextControl.Invalidate();
215+
};
216+
217+
this.richTextControl.MouseUp += (_, e) =>
218+
{
219+
this.richTextScene.OnMouseUp(e);
220+
if (e.Button == MouseButtons.Left)
221+
{
222+
this.richTextControl.Capture = false;
223+
}
224+
225+
this.UpdateEditorToolbar();
226+
this.richTextControl.Invalidate();
227+
};
228+
229+
this.richTextControl.PreviewKeyDown += (_, e) =>
230+
{
231+
e.IsInputKey = e.KeyCode is Keys.Left or Keys.Right or Keys.Up or Keys.Down or Keys.Home or Keys.End;
232+
};
233+
234+
this.richTextControl.KeyDown += this.OnRichTextKeyDown;
235+
this.richTextControl.KeyPress += this.OnRichTextKeyPress;
236+
102237
TabControl tabs = new() { Dock = DockStyle.Fill };
103238

104239
TabPage clockTab = new(this.clockScene.DisplayName);
@@ -113,7 +248,16 @@ public MainForm()
113248
applyTab.Controls.Add(this.applyControl);
114249
tabs.TabPages.Add(applyTab);
115250

251+
TabPage manualTextFlowTab = new(this.manualTextFlowScene.DisplayName);
252+
manualTextFlowTab.Controls.Add(this.manualTextFlowControl);
253+
tabs.TabPages.Add(manualTextFlowTab);
254+
255+
TabPage richTextTab = new(this.richTextScene.DisplayName);
256+
richTextTab.Controls.Add(richTextPanel);
257+
tabs.TabPages.Add(richTextTab);
258+
116259
this.Controls.Add(tabs);
260+
this.UpdateEditorToolbar();
117261
}
118262

119263
private void OnPaintClock(DrawingCanvas canvas, TimeSpan delta)
@@ -127,4 +271,95 @@ private void OnPaintTiger(DrawingCanvas canvas, TimeSpan delta)
127271

128272
private void OnPaintApply(DrawingCanvas canvas, TimeSpan delta)
129273
=> this.applyScene.Paint(canvas, delta);
274+
275+
private void OnPaintManualTextFlow(DrawingCanvas canvas, TimeSpan delta)
276+
=> this.manualTextFlowScene.Paint(canvas, delta);
277+
278+
private void OnPaintRichText(DrawingCanvas canvas, TimeSpan delta)
279+
=> this.richTextScene.Paint(canvas, delta);
280+
281+
private void OnRichTextKeyDown(object? sender, KeyEventArgs e)
282+
{
283+
if (!this.richTextScene.OnKeyDown(e))
284+
{
285+
return;
286+
}
287+
288+
e.SuppressKeyPress = true;
289+
this.UpdateEditorToolbar();
290+
this.richTextControl.Invalidate();
291+
}
292+
293+
private void OnRichTextKeyPress(object? sender, KeyPressEventArgs e)
294+
{
295+
if (!this.richTextScene.OnKeyPress(e.KeyChar))
296+
{
297+
return;
298+
}
299+
300+
e.Handled = true;
301+
this.UpdateEditorToolbar();
302+
this.richTextControl.Invalidate();
303+
}
304+
305+
private CheckBox CreateEditorToggleButton(string text, Action action)
306+
{
307+
CheckBox button = new()
308+
{
309+
Appearance = Appearance.Button,
310+
AutoSize = true,
311+
Text = text,
312+
Font = new Font(FontFamily.GenericSansSerif, 9F, FontStyle.Bold),
313+
Margin = new Padding(0, 0, 6, 0),
314+
};
315+
316+
button.Click += (_, _) => this.InvokeEditorCommand(action);
317+
return button;
318+
}
319+
320+
private Button CreateEditorButton(string text, Action action)
321+
{
322+
Button button = new()
323+
{
324+
AutoSize = true,
325+
Text = text,
326+
Margin = new Padding(0, 0, 6, 0),
327+
};
328+
329+
button.Click += (_, _) => this.InvokeEditorCommand(action);
330+
return button;
331+
}
332+
333+
private Button CreateEditorColorButton(Color color, ImageSharpColor textColor)
334+
{
335+
Button button = new()
336+
{
337+
BackColor = color,
338+
FlatStyle = FlatStyle.Flat,
339+
Margin = new Padding(0, 1, 6, 0),
340+
Size = new Size(28, 24),
341+
UseVisualStyleBackColor = false,
342+
};
343+
344+
button.Click += (_, _) => this.InvokeEditorCommand(() => this.richTextScene.SetFillColor(textColor));
345+
return button;
346+
}
347+
348+
private void InvokeEditorCommand(Action action)
349+
{
350+
action();
351+
this.UpdateEditorToolbar();
352+
this.richTextControl.Focus();
353+
this.richTextControl.Invalidate();
354+
}
355+
356+
private void UpdateEditorToolbar()
357+
{
358+
this.boldButton.Checked = this.richTextScene.IsBold;
359+
this.italicButton.Checked = this.richTextScene.IsItalic;
360+
this.underlineButton.Checked = this.richTextScene.IsUnderline;
361+
this.strikeoutButton.Checked = this.richTextScene.IsStrikeout;
362+
this.fontSizeLabel.Text = $"{this.richTextScene.CurrentFontSize:0.#} pt";
363+
this.selectionStatusLabel.Text = $"Selected: {this.richTextScene.SelectionLength}";
364+
}
130365
}

samples/WebGPUExternalSurfaceDemo/README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,13 @@ Requirements:
2323
- a WebGPU-capable desktop backend such as D3D12 or Vulkan
2424
- adapter support for the storage-capable BGRA format selected by the sample
2525

26-
When the sample starts you should see a WinForms window with three tabs:
26+
When the sample starts you should see a WinForms window with five tabs:
2727

2828
- `Clock`: a continuously-rendered animated clock scene
2929
- `Tiger`: an interactive SVG tiger viewer with pan and zoom
3030
- `Apply`: a reactive external-surface readback scene using `DrawingCanvas.Apply(...)`; move the mouse to move the edge-detect and blur regions, and use the mouse wheel to resize them
31+
- `Manual Text Flow`: prepared text is laid out one line at a time around a circular obstacle that follows mouse movement
32+
- `Rich Text Editor`: a small editor surface that exercises text selection, caret movement, hit testing, font changes, and inline styling
3133

3234
## Why This Sample Matters
3335

@@ -114,6 +116,8 @@ The scenes are deliberately ordinary canvas code:
114116
- [ClockScene.cs](d:/GitHub/SixLabors/ImageSharp.Drawing/samples/WebGPUExternalSurfaceDemo/Scenes/ClockScene.cs): animated vector clock
115117
- [TigerViewerScene.cs](d:/GitHub/SixLabors/ImageSharp.Drawing/samples/WebGPUExternalSurfaceDemo/Scenes/TigerViewerScene.cs): pan and zoom SVG tiger viewer
116118
- [ApplyReadbackScene.cs](d:/GitHub/SixLabors/ImageSharp.Drawing/samples/WebGPUExternalSurfaceDemo/Scenes/ApplyReadbackScene.cs): `Apply(...)` scene that reads the external surface back into CPU processing
119+
- [ManualTextFlowScene.cs](d:/GitHub/SixLabors/ImageSharp.Drawing/samples/WebGPUExternalSurfaceDemo/Scenes/ManualTextFlowScene.cs): interactive manual text flow using prepared line layout enumeration
120+
- [RichTextEditorScene.cs](d:/GitHub/SixLabors/ImageSharp.Drawing/samples/WebGPUExternalSurfaceDemo/Scenes/RichTextEditorScene.cs): custom rich text editor built from Fonts hit testing, caret, selection, and run APIs
117121

118122
Each scene receives:
119123

@@ -127,4 +131,6 @@ Each scene receives:
127131
- [ClockScene.cs](d:/GitHub/SixLabors/ImageSharp.Drawing/samples/WebGPUExternalSurfaceDemo/Scenes/ClockScene.cs): clock scene
128132
- [TigerViewerScene.cs](d:/GitHub/SixLabors/ImageSharp.Drawing/samples/WebGPUExternalSurfaceDemo/Scenes/TigerViewerScene.cs): tiger viewer scene
129133
- [ApplyReadbackScene.cs](d:/GitHub/SixLabors/ImageSharp.Drawing/samples/WebGPUExternalSurfaceDemo/Scenes/ApplyReadbackScene.cs): apply readback scene
134+
- [ManualTextFlowScene.cs](d:/GitHub/SixLabors/ImageSharp.Drawing/samples/WebGPUExternalSurfaceDemo/Scenes/ManualTextFlowScene.cs): manual text-flow scene
135+
- [RichTextEditorScene.cs](d:/GitHub/SixLabors/ImageSharp.Drawing/samples/WebGPUExternalSurfaceDemo/Scenes/RichTextEditorScene.cs): rich text editor scene
130136
- [WebGPUExternalSurfaceDemo.csproj](d:/GitHub/SixLabors/ImageSharp.Drawing/samples/WebGPUExternalSurfaceDemo/WebGPUExternalSurfaceDemo.csproj): sample project file

0 commit comments

Comments
 (0)