--- /dev/null
+// Copyright (c) 2013-2021 Jean-Philippe Bruyère <jp_bruyere@hotmail.com>
+//
+// This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT)
+
+using Crow.Text;
+using System;
+
+namespace Crow
+{
+ /// <summary>
+ /// Occurs in the TextBox widget when the text has changed.
+ /// </summary>
+ public class SelectedTextChangeEventArgs: EventArgs
+ {
+ /// <summary>
+ /// The TextChange structure representing the change.
+ /// </summary>
+ public TextSpan Old;
+ public TextSpan New;
+
+ public SelectedTextChangeEventArgs (TextSpan old, TextSpan _new) : base()
+ {
+ Old = old;
+ New = _new;
+ }
+ }
+}
+
-// Copyright (c) 2013-2020 Jean-Philippe Bruyère <jp_bruyere@hotmail.com>
+// Copyright (c) 2013-2021 Jean-Philippe Bruyère <jp_bruyere@hotmail.com>
//
// This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT)
-using System;
+// Copyright (c) 2013-2021 Jean-Philippe Bruyère <jp_bruyere@hotmail.com>
+//
+// This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT)
+using System;
using System.Diagnostics;
namespace Crow.Text
-using System;
+// Copyright (c) 2013-2021 Jean-Philippe Bruyère <jp_bruyere@hotmail.com>
+//
+// This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT)
+using System;
using System.Collections.Generic;
using System.Text;
-using System;
+// Copyright (c) 2013-2021 Jean-Philippe Bruyère <jp_bruyere@hotmail.com>
+//
+// This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT)
+using System;
namespace Crow.Text
{
+// Copyright (c) 2013-2021 Jean-Philippe Bruyère <jp_bruyere@hotmail.com>
+//
+// This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT)
using System;
using Crow.Cairo;
-using System;
+// Copyright (c) 2013-2021 Jean-Philippe Bruyère <jp_bruyere@hotmail.com>
+//
+// This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT)
+using System;
using System.Collections.Generic;
using System.Text;
-using System;
+// Copyright (c) 2013-2021 Jean-Philippe Bruyère <jp_bruyere@hotmail.com>
+//
+// This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT)
+using System;
using System.Collections.Generic;
using System.Text;
-using System;
+// Copyright (c) 2013-2021 Jean-Philippe Bruyère <jp_bruyere@hotmail.com>
+//
+// This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT)
+using System;
using System.Collections.Generic;
using System.Text;
-using System;
+// Copyright (c) 2013-2021 Jean-Philippe Bruyère <jp_bruyere@hotmail.com>
+//
+// This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT)
+using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
-using System;
+// Copyright (c) 2013-2021 Jean-Philippe Bruyère <jp_bruyere@hotmail.com>
+//
+// This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT)
+using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
-using System;
+// Copyright (c) 2013-2021 Jean-Philippe Bruyère <jp_bruyere@hotmail.com>
+//
+// This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT)
+using System;
using System.Collections.Generic;
using System.Text;
namespace Crow.Text
{
- public struct TextSpan
+ public struct TextSpan : IEquatable<TextSpan>
{
public readonly int Start;
public readonly int End;
public bool IsEmpty => Start == End;
public int Length => End - Start;
+
+ public bool Equals(TextSpan other)
+ => Start == other.Start && End == other.End;
+ public override bool Equals(object obj)
+ => obj is TextSpan ts ? Equals(ts) : false;
+
+ public override int GetHashCode()
+ => HashCode.Combine(Start, End);
+ public static bool operator ==(TextSpan left, TextSpan right)
+ => left.Equals (right);
+ public static bool operator !=(TextSpan left, TextSpan right)
+ => !left.Equals (right);
}
}
-// Copyright (c) 2013-2020 Jean-Philippe Bruyère <jp_bruyere@hotmail.com>
+// Copyright (c) 2013-2021 Jean-Philippe Bruyère <jp_bruyere@hotmail.com>
//
// This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT)
}
}
- Unit u1, u2;
- int init1 = -1, init2 = -1, delta = 0, min1, min2, max1 , max2;
- Widget go1 = null, go2 = null;
-
- void initSplit(Measure m1, int size1, Measure m2, int size2){
- if (m1 != Measure.Stretched) {
- init1 = size1;
- u1 = m1.Units;
- }
- if (m2 != Measure.Stretched) {
- init2 = size2;
- u2 = m2.Units;
- }
- }
- void convertSizeInPix(Widget g1){
-
- }
-
#region GraphicObject override
public override ILayoutable Parent {
- get { return base.Parent; }
+ get => base.Parent;
set {
if (value != null) {
GenericStack gs = value as GenericStack;
else
IFace.MouseCursor = MouseCursor.sb_v_double_arrow;
}
- public override void onMouseDown (object sender, MouseButtonEventArgs e)
- {
- go1 = go2 = null;
- init1 = init2 = -1;
- delta = 0;
-
+
+ public override void onMouseMove (object sender, MouseMoveEventArgs e)
+ {
GenericStack gs = Parent as GenericStack;
+ Point m = gs.ScreenPointToLocal (e.Position);
int ptrSplit = gs.Children.IndexOf (this);
- if (ptrSplit == 0 || ptrSplit == gs.Children.Count - 1)
- return;
-
- go1 = gs.Children [ptrSplit - 1];
- go2 = gs.Children [ptrSplit + 1];
-
- if (gs.Orientation == Orientation.Horizontal) {
- initSplit (go1.Width, go1.Slot.Width, go2.Width, go2.Slot.Width);
- min1 = go1.MinimumSize.Width;
- min2 = go2.MinimumSize.Width;
- max1 = go1.MaximumSize.Width;
- max2 = go2.MaximumSize.Width;
- if (init1 >= 0)
- go1.Width = init1;
- if (init2 >= 0)
- go2.Width = init2;
- } else {
- initSplit (go1.Height, go1.Slot.Height, go2.Height, go2.Slot.Height);
- min1 = go1.MinimumSize.Height;
- min2 = go2.MinimumSize.Height;
- max1 = go1.MaximumSize.Height;
- max2 = go2.MaximumSize.Height;
- if (init1 >= 0)
- go1.Height = init1;
- if (init2 >= 0)
- go2.Height = init2;
- }
- e.Handled = true;
- base.onMouseDown (sender, e);
- }
- public override void onMouseMove (object sender, MouseMoveEventArgs e)
- {
- e.Handled = true;
- base.onMouseMove (sender, e);
-
- if (IsActive && go1 != null && go2 != null) {
- GenericStack gs = Parent as GenericStack;
- int newDelta = delta, size1 = init1, size2 = init2;
- if (gs.Orientation == Orientation.Horizontal) {
- newDelta -= e.XDelta;
- if (size1 < 0)
- size1 = go1.Slot.Width + delta;
- if (size2 < 0)
- size2 = go2.Slot.Width - delta;
- } else {
- newDelta -= e.YDelta;
- if (size1 < 0)
- size1 = go1.Slot.Height + delta;
- if (size2 < 0)
- size2 = go2.Slot.Height - delta;
- }
- if (size1 - newDelta < min1 || (max1 > 0 && size1 - newDelta > max1) ||
- size2 + newDelta < min2 || (max2 > 0 && size2 + newDelta > max2))
- return;
-
- delta = newDelta;
-
- if (gs.Orientation == Orientation.Horizontal) {
- if (init1 >= 0)
- go1.Width = init1 - delta;
- if (init2 >= 0)
- go2.Width = init2 + delta;
+ if (IFace.IsDown (Glfw.MouseButton.Left) && ptrSplit > 0 && ptrSplit < gs.Children.Count - 1) {
+ Widget w0 = gs.Children[ptrSplit - 1];
+ Widget w1 = gs.Children[ptrSplit + 1];
+ if (gs.Orientation == Orientation.Horizontal) {
+ int x = m.X - Slot.Width / 2 - gs.Spacing;
+
+ if (x > w0.Slot.Left + w0.MinimumSize.Width &&
+ x + Slot.Width + 2 * gs.Spacing < w1.Slot.Right - w1.MinimumSize.Width) {
+ w0.Width = x - w0.Slot.X;
+ x += Slot.Width + 2 * gs.Spacing;
+ w1.Width = w1.Slot.Right - x;
+ }
} else {
- if (init1 >= 0)
- go1.Height = init1 - delta;
- if (init2 >= 0)
- go2.Height = init2 + delta;
+ int y = m.Y - Slot.Height / 2 - gs.Spacing;
+
+ if (y > w0.Slot.Top + w0.MinimumSize.Height &&
+ y + Slot.Height + 2 * gs.Spacing < w1.Slot.Bottom - w1.MinimumSize.Height) {
+ w0.Height = y - w0.Slot.Top;
+ y += Slot.Height + 2 * gs.Spacing;
+ w1.Height = w1.Slot.Bottom - y;
+ }
}
-
+ e.Handled = true;
}
- }
- public override void onMouseUp (object sender, MouseButtonEventArgs e)
- {
- base.onMouseUp (sender, e);
-
- GenericStack gs = Parent as GenericStack;
- if (init1 >= 0 && u1 == Unit.Percent) {
- if (gs.Orientation == Orientation.Horizontal)
- go1.Width = new Measure ((int)Math.Ceiling (
- go1.Width.Value * 100.0 / (double)(gs.Slot.Width - 2 * gs.Margin)), Unit.Percent);
- else
- go1.Height = new Measure ((int)Math.Ceiling (
- go1.Height.Value * 100.0 / (double)gs.Slot.Height), Unit.Percent);
- }
- if (init2 >= 0 && u2 == Unit.Percent) {
- if (gs.Orientation == Orientation.Horizontal)
- go2.Width = new Measure ((int)Math.Floor (
- go2.Width.Value * 100.0 / (double)(gs.Slot.Width - 2 * gs.Margin)), Unit.Percent);
- else
- go2.Height = new Measure ((int)Math.Floor (
- go2.Height.Value * 100.0 / (double)gs.Slot.Height), Unit.Percent);
- }
+
+ base.onMouseMove (sender, e);
}
+
public override bool UpdateLayout (LayoutingType layoutType)
{
GenericStack gs = Parent as GenericStack;
Height = Measure.Stretched;
}
return base.UpdateLayout (layoutType);
- }
- public override bool PointIsIn (ref Point m)
- {
- if (!(Visible & IsEnabled)||IsDragged)
- return false;
- if (!Parent.PointIsIn(ref m))
- return false;
- m -= (Parent.getSlot().Position + Parent.ClientRectangle.Position) ;
- Rectangle r = Slot;
- if (Width == Measure.Stretched)
- r.Inflate (0, 5);
- else
- r.Inflate (5, 0);
- return r.ContainsOrIsEqual (m);
- }
+ }
#endregion
}
}
string caption;
Measure width = Measure.Fit;
- //public int ComputedWidth;
+ public int ComputedWidth;
public Widget LargestWidget;
public string Caption {
//int lineWidth;
ObservableList<Column> columns = new ObservableList<Column>();
- /*[DefaultValue (1)]
- public int LineWidth {
- get => lineWidth;
- set {
- if (lineWidth == value)
- return;
- lineWidth = value;
- NotifyValueChangedAuto (lineWidth);
- RegisterForLayouting (LayoutingType.Sizing | LayoutingType.ArrangeChildren);
- }
- }*/
-
public ObservableList<Column> Columns {
get => columns;
set {
layoutType &= (~(LayoutingType.X|LayoutingType.Width));
}*/
+ //overriden to prevent search for largest child, all the rows as the same total width.
public override void ComputeChildrenPositions () {
int d = 0;
childrenRWLock.EnterReadLock();
RegisteredLayoutings &= (~layoutType);
if (layoutType == LayoutingType.Width) {
+ //propagate column.width to each row's children
foreach (TableRow row in Children) {
for (int i = 0; i < Columns.Count && i < row.Children.Count; i++)
row.Children[i].Width = Columns[i].Width;
}
#endregion
- int spacing;
-
public Table Table => Parent as Table;
/*public override void ChildrenLayoutingConstraints(ILayoutable layoutable, ref LayoutingType layoutType)
return;
int spacing = Table.Spacing;
ObservableList<Column> cols = Table.Columns;
-
- //int d = 0;
+
Widget first = Children[0];
TableRow firstRow = Table.Children[0] as TableRow;
+
if (firstRow == this) {
base.ComputeChildrenPositions();
return;
}
- childrenRWLock.EnterReadLock();
+ childrenRWLock.EnterReadLock();
+
for (int i = 0; i < Children.Count && i < firstRow.Children.Count; i++)
{
- Widget w = Children[i];
- w.Slot.X = firstRow.Children[i].Slot.X;
- setChildWidth (w, firstRow.Children[i].Slot.Width);
- //d += spacing + firstRow.Children[i].Slot.Width;
+ Widget w = Children[i];
+ /*if (i < cols.Count && cols[i].Width.IsFit && cols[i].LargestWidget != null) {
+ w.Slot.X
+ }else{*/
+ w.Slot.X = firstRow.Children[i].Slot.X;
+ setChildWidth (w, firstRow.Children[i].Slot.Width);
+ //}
}
childrenRWLock.ExitReadLock();
if (layoutType == LayoutingType.Width) {
if (firstRow.RegisteredLayoutings.HasFlag (LayoutingType.Width))
return false;
- if (this != firstRow) {
- //contentSize = firstRow.contentSize;
+ if (this != firstRow) {
Slot.Width = firstRow.Slot.Width;
if (Slot.Width != LastSlots.Width) {
IsDirty = true;
}
return base.measureRawSize (lt);
}*/
- public override void OnLayoutChanges(LayoutingType layoutType)
- {
- base.OnLayoutChanges(layoutType);
- }
+ /*public override void OnChildLayoutChanges (object sender, LayoutingEventArgs arg) {
+ if (arg.LayoutType == LayoutingType.Width) {
+ Widget w = sender as Widget;
+ TableRow firstRow = Table.Children[0] as TableRow;
+ int c = Children.IndexOf(w);
+ if (c < Table.Columns.Count) {
+ Column col = Table.Columns[c];
+ if (col.Width.IsFit) {
+ if (col.LargestWidget == null || w.Slot.Width > col.ComputedWidth) {
+ col.ComputedWidth = w.Slot.Width;
+ col.LargestWidget = w;
+ } else if (w == col.LargestWidget)
+ Console.WriteLine ("must search for largest widget");
+ }else if (w == firstRow)
+ col.ComputedWidth = w.Slot.Width;
+
+ }
+ Console.WriteLine ($"ROW:{Table.Children.IndexOf(this)} COL:{c} {w.LastSlots.Width} -> {w.Slot.Width} ");
+ }
+ base.OnChildLayoutChanges (sender, arg);
+ }*/
/*public override void OnChildLayoutChanges (object sender, LayoutingEventArgs arg) {
Widget go = sender as Widget;
TableRow row = go.Parent as TableRow;
splitPos -= Spacing;
Table.Columns[splitIndex].Width = splitPos - firstRow.Children[splitIndex].Slot.Left;
Table.RegisterForLayouting (LayoutingType.Width);
+ e.Handled = true;
}
//Console.WriteLine ($"left:{firstRow.Children[splitIndex].Slot.Left} right:{firstRow.Children[splitIndex+1].Slot.Right} cb.X:{cb.X} splitPos:{splitPos} m:{m}");
} else {
if (m.X >= r.Right) {
r = Children[i+1].Slot;
if (m.X <= r.Left && Table.Columns.Count - 1 > i ) {
- Console.WriteLine ($"Set cursor Table row on mouse move. {m}");
IFace.MouseCursor = MouseCursor.sb_h_double_arrow;
splitIndex = i;
e.Handled = true;
}
}
}
- if (splitIndex < 0) {
+ if (splitIndex < 0 && IFace.MouseCursor == MouseCursor.sb_h_double_arrow)
IFace.MouseCursor = MouseCursor.top_left_arrow;
- Console.WriteLine ($"RESet cursor Table row on mouse move. {m}");
- }
}
}
base.onMouseMove(sender, e);
if (width == value)
return;
if (value.IsFixed) {
- if (value < minimumSize.Width || (value > maximumSize.Width && maximumSize.Width > 0))
+ if (value < minimumSize.Width || (maximumSize.Width > 0 && value > maximumSize.Width))
return;
}
width = value;
if (height == value)
return;
if (value.IsFixed) {
- if (value < minimumSize.Height || (value > maximumSize.Height && maximumSize.Height > 0))
+ if (value < minimumSize.Height || (maximumSize.Height > 0 && value > maximumSize.Height))
return;
}
height = value;
CMDUndo, CMDRedo, CMDCut, CMDCopy, CMDPaste, CMDHelp, CMDAbout, CMDOptions;
const string _defaultFileName = "unnamed.txt";
- string source = "";
- int dirtyUndoLevel;
+ string source = "", origSource;
TextBox editor;
Stopwatch reloadChrono = new Stopwatch ();
- public new bool IsDirty { get { return undoStack.Count != dirtyUndoLevel; } }
+ public new bool IsDirty => source != origSource;
public string Source {
get => source;
set {
if (source == value)
return;
source = value;
+ CMDSave.CanExecute = IsDirty;
if (!reloadChrono.IsRunning)
reloadChrono.Restart ();
NotifyValueChanged (source);
+ NotifyValueChanged ("IsDirty", IsDirty);
}
}
public CommandGroup EditorCommands => new CommandGroup (CMDUndo, CMDRedo, CMDCut, CMDCopy, CMDPaste, CMDSave, CMDSaveAs);
Stack<TextChange> undoStack = new Stack<TextChange> ();
Stack<TextChange> redoStack = new Stack<TextChange> ();
+ TextSpan selection;
+ string SelectedText =>
+ selection.IsEmpty ? "" : Source.AsSpan (selection.Start, selection.Length).ToString ();
void undo () {
if (undoStack.TryPop (out TextChange tch)) {
if (redoStack.Count == 0)
CMDRedo.CanExecute = false;
}
+ void cut () {
+ copy ();
+ applyChange (new TextChange (selection.Start, selection.Length, ""));
+ }
+ void copy () {
+ Clipboard = SelectedText;
+ }
+ void paste () {
+ applyChange (new TextChange (selection.Start, selection.Length, Clipboard));
+ }
bool disableTextChangedEvent = false;
void apply (TextChange change) {
Span<char> tmp = stackalloc char[source.Length + (change.ChangedText.Length - change.Length)];
src.Slice (change.End).CopyTo (tmp.Slice (change.Start + change.ChangedText.Length));
disableTextChangedEvent = true;
Source = tmp.ToString ();
- disableTextChangedEvent = false;
- NotifyValueChanged ("IsDirty", IsDirty);
+ disableTextChangedEvent = false;
}
void initCommands ()
CMDQuit = new Command (new Action (() => base.Quit ())) { Caption = "Quit", Icon = "#Icons.exit.svg", CanExecute = true };
CMDUndo = new Command (new Action (undo)) { Caption = "Undo", Icon = "#Icons.undo.svg", CanExecute = false };
CMDRedo = new Command (new Action (redo)) { Caption = "Redo", Icon = "#Icons.redo.svg", CanExecute = false };
- CMDCut = new Command (new Action (() => Quit ())) { Caption = "Cut", Icon = "#Icons.scissors.svg", CanExecute = false };
- CMDCopy = new Command (new Action (() => Quit ())) { Caption = "Copy", Icon = "#Icons.copy-file.svg", CanExecute = false };
- CMDPaste = new Command (new Action (() => Quit ())) { Caption = "Paste", Icon = "#Icons.paste-on-document.svg", CanExecute = false };
+ CMDCut = new Command (new Action (() => cut ())) { Caption = "Cut", Icon = "#Icons.scissors.svg", CanExecute = false };
+ CMDCopy = new Command (new Action (() => copy ())) { Caption = "Copy", Icon = "#Icons.copy-file.svg", CanExecute = false };
+ CMDPaste = new Command (new Action (() => paste ())) { Caption = "Paste", Icon = "#Icons.paste-on-document.svg", CanExecute = false };
}
void onNewFile () {
byte [] buff = Encoding.UTF8.GetBytes (source);
s.Write (buff, 0, buff.Length);
}
- dirtyUndoLevel = undoStack.Count;
+ origSource = source;
NotifyValueChanged ("IsDirty", IsDirty);
+ CMDSave.CanExecute = false;
}
void reloadFromFile () {
if (File.Exists (CurrentFile)) {
using (Stream s = new FileStream (CurrentFile, FileMode.Open)) {
using (StreamReader sr = new StreamReader (s))
- Source = sr.ReadToEnd ();
+ Source = origSource = sr.ReadToEnd ();
}
}
disableTextChangedEvent = false;
undoStack.Clear ();
redoStack.Clear ();
CMDUndo.CanExecute = false;
- CMDRedo.CanExecute = false;
- dirtyUndoLevel = 0;
+ CMDRedo.CanExecute = false;
}
void showError (Exception ex) {
NotifyValueChanged ("ErrorMessage", (object)ex.Message);
void onTextChanged (object sender, TextChangeEventArgs e) {
if (disableTextChangedEvent)
return;
- undoStack.Push (e.Change.Inverse (source));
+ applyChange (e.Change);
+ }
+ void applyChange (TextChange change) {
+ undoStack.Push (change.Inverse (source));
redoStack.Clear ();
CMDUndo.CanExecute = true;
CMDRedo.CanExecute = false;
- apply (e.Change);
+ apply (change);
+ }
+
+ void onSelectedTextChanged (object sender, EventArgs e) {
+ selection = (sender as Label).Selection;
+ Console.WriteLine($"selection:{selection.Start} length:{selection.Length}");
+ CMDCut.CanExecute = CMDCopy.CanExecute = !selection.IsEmpty;
}
void textView_KeyDown (object sender, Crow.KeyEventArgs e) {
- if (Ctrl && e.Key == Glfw.Key.W) {
- if (Shift)
- CMDRedo.Execute ();
- else
- CMDUndo.Execute ();
+ if (Ctrl) {
+ if (e.Key == Glfw.Key.W) {
+ if (Shift)
+ CMDRedo.Execute ();
+ else
+ CMDUndo.Execute ();
+ } else if (e.Key == Glfw.Key.S) {
+ onSave ();
+ }
}
}
<Button Style="IcoButton" Command="{CMDSaveAs}" />
<Button Style="IcoButton" Command="{CMDUndo}" />
<Button Style="IcoButton" Command="{CMDRedo}" />
+ <Button Style="IcoButton" Command="{CMDCut}" />
+ <Button Style="IcoButton" Command="{CMDCopy}" />
+ <Button Style="IcoButton" Command="{CMDPaste}" />
<Widget Width="Stretched"/>
<Label Text="Line:" Foreground="Grey"/>
<Label Text="{../../tb.CurrentLine}" Margin="2"/>
<Label Text="{../../tb.CurrentColumn}" Margin="2"/>
</HorizontalStack>
<HorizontalStack>
- <TextBox Name="tb" Text="{Source}" Multiline="true" Font="consolas, 12" Focusable="true" Height="Stretched" Width="Stretched"
- TextChanged="onTextChanged" KeyDown="textView_KeyDown" ContextCommands="{ContextCommands}"/>
+ <TextBox Name="tb" Text="{Source}" Multiline="true" Font="consolas, 12" Focusable="true" Height="Stretched" Width="Stretched"
+ TextChanged="onTextChanged" KeyDown="textView_KeyDown" ContextCommands="{EditorCommands}"/>
+ <!--SelectionChanged="onSelectedTextChanged"-->
<ScrollBar Value="{²../tb.ScrollY}"
LargeIncrement="{../tb.PageHeight}" SmallIncrement="1"
CursorRatio="{../tb.ChildHeightRatio}" Maximum="{../tb.MaxScrollY}" />
<ScrollBar Style="HScrollBar" Value="{²../tb.ScrollX}"
LargeIncrement="{../tb.PageWidth}" SmallIncrement="1"
CursorRatio="{../tb.ChildWidthRatio}" Maximum="{../tb.MaxScrollX}" />
- <Label Text="{CurrentFile}" Width="Stretched"/>
+ <HorizontalStack Height="Fit">
+ <Widget Width="10" Height="10" Background="RoyalBlue" Visible="{IsDirty}"/>
+ <Label Text="{CurrentFile}" Width="Stretched"/>
+ </HorizontalStack>
<Label Visible="{ShowError}" Text="{ErrorMessage}" Background="Red" Foreground="White" Width="Stretched" Margin="2"
Multiline="true"/>
</VerticalStack>