Merge branch 'v2_2144-3D-effect' of tig:tig/Terminal.Gui into v2_2144-3D-effect

This commit is contained in:
Tig
2024-06-24 09:05:27 -07:00
22 changed files with 416 additions and 24 deletions

View File

@@ -973,6 +973,7 @@ public static partial class Application
if (state.Toplevel.NeedsDisplay || state.Toplevel.SubViewNeedsDisplay || state.Toplevel.LayoutNeeded || OverlappedChildNeedsDisplay ())
{
state.Toplevel.SetNeedsDisplay();
state.Toplevel.Draw ();
Driver.UpdateScreen ();

View File

@@ -259,6 +259,27 @@ public readonly partial record struct Color : ISpanParsable<Color>, IUtf8SpanPar
}
/// <summary>
/// Gets a color that is the same hue as the current color, but with a different lightness.
/// </summary>
/// <returns></returns>
public Color GetDarkerColor ()
{
// TODO: This is a temporary implementation; just enough to show how it could work.
var hsl = ColorHelper.ColorConverter.RgbToHsl (new RGB (R, G, B));
var amount = .3;
if (hsl.L <= 5)
{
return DarkGray;
}
hsl.L = (byte)(hsl.L * amount);
var rgb = ColorHelper.ColorConverter.HslToRgb (hsl);
return new (rgb.R, rgb.G, rgb.B);
}
#region Legacy Color Names
/// <summary>The black color.</summary>

View File

@@ -1,4 +1,5 @@
namespace Terminal.Gui;
#nullable enable
namespace Terminal.Gui;
/// <summary>The Margin for a <see cref="View"/>.</summary>
/// <remarks>
@@ -15,8 +16,64 @@ public class Margin : Adornment
public Margin (View parent) : base (parent)
{
/* Do nothing; View.CreateAdornment requires a constructor that takes a parent */
HighlightStyle |= HighlightStyle.Pressed;
Highlight += Margin_Highlight;
LayoutStarted += Margin_LayoutStarted;
}
private void Margin_LayoutStarted (object? sender, LayoutEventArgs e)
{
// Adjust the shadow such that it is drawn aligned with the Border
if (_shadow && _rightShadow is {} && _bottomShadow is {})
{
_rightShadow.Y = Parent.Border.Thickness.Top - (Parent.Border.Thickness.Top > 2 && Parent.Border.ShowTitle ? 1 : 0);
_bottomShadow.X = Parent.Border.Thickness.Left;
}
}
private bool _pressed;
private void Margin_Highlight (object? sender, HighlightEventArgs e)
{
if (_shadow)
{
if (_pressed && e.HighlightStyle == HighlightStyle.None)
{
Thickness = new (Thickness.Left - 1, Thickness.Top, Thickness.Right + 1, Thickness.Bottom);
if (_rightShadow is { })
{
_rightShadow.Visible = true;
}
if (_bottomShadow is { })
{
_bottomShadow.Visible = true;
}
_pressed = false;
return;
}
if (!_pressed && (e.HighlightStyle.HasFlag (HighlightStyle.Pressed) /*|| e.HighlightStyle.HasFlag (HighlightStyle.PressedOutside)*/))
{
Thickness = new (Thickness.Left + 1, Thickness.Top, Thickness.Right - 1, Thickness.Bottom);
_pressed = true;
if (_rightShadow is { })
{
_rightShadow.Visible = false;
}
if (_bottomShadow is { })
{
_bottomShadow.Visible = false;
}
}
}
}
/// <summary>
/// The color scheme for the Margin. If set to <see langword="null"/>, gets the <see cref="Adornment.Parent"/>'s
/// <see cref="View.SuperView"/> scheme. color scheme.
@@ -30,7 +87,7 @@ public class Margin : Adornment
return base.ColorScheme;
}
return Parent?.SuperView?.ColorScheme ?? Colors.ColorSchemes ["TopLevel"];
return (Parent?.SuperView?.ColorScheme ?? Colors.ColorSchemes ["TopLevel"])!;
}
set
{
@@ -38,4 +95,157 @@ public class Margin : Adornment
Parent?.SetNeedsDisplay ();
}
}
private bool _shadow;
/// <summary>
/// Gets or sets whether the Margin includes a shadow effect. The shadow is drawn on the right and bottom sides of the
/// Margin.
/// </summary>
public bool EnableShadow (bool enable)
{
if (_shadow == enable)
{
return _shadow;
}
if (_shadow)
{
Thickness = new (Thickness.Left, Thickness.Top, Thickness.Right - 1, Thickness.Bottom - 1);
}
_shadow = enable;
if (_shadow)
{
Thickness = new (Thickness.Left, Thickness.Top, Thickness.Right + 1, Thickness.Bottom + 1);
}
if (_rightShadow is { })
{
_rightShadow.Visible = _shadow;
}
if (_bottomShadow is { })
{
_bottomShadow.Visible = _shadow;
}
return _shadow;
}
private View? _bottomShadow;
private View? _rightShadow;
/// <inheritdoc/>
public override void BeginInit ()
{
base.BeginInit ();
if (Parent is null)
{
return;
}
Attribute attr = Parent.GetNormalColor ();
Add (
_rightShadow = new ShadowView
{
X = Pos.AnchorEnd (1),
Y = 0,
Width = 1,
Height = Dim.Fill (),
Visible = _shadow,
Orientation = Orientation.Vertical
},
_bottomShadow = new ShadowView
{
X = 0,
Y = Pos.AnchorEnd (1),
Width = Dim.Fill (),
Height = 1,
Visible = _shadow,
Orientation = Orientation.Horizontal
}
);
}
}
/// <summary>
/// Draws a shadow on the right or bottom of the view.
/// </summary>
internal class ShadowView : View
{
// TODO: Add these to CM.Glyphs
private readonly char VERTICAL_START_GLYPH = '\u2596';
private readonly char VERTICAL_GLYPH = '\u258C';
private readonly char HORIZONTAL_START_GLYPH = '\u259d';
private readonly char HORIZONTAL_GLYPH = '\u2580';
private readonly char HORIZONTAL_END_GLYPH = '\u2598';
/// <summary>
/// Gets or sets the orientation of the shadow.
/// </summary>
public Orientation Orientation { get; set; }
/// <inheritdoc />
public override Attribute GetNormalColor ()
{
if (SuperView is Adornment adornment)
{
if (adornment.Parent.SuperView is { })
{
Attribute attr = adornment.Parent.SuperView.GetNormalColor ();
return new (new Attribute (attr.Foreground.GetDarkerColor (), attr.Background));
}
else
{
Attribute attr = Application.Top.GetNormalColor ();
return new (new Attribute (attr.Foreground.GetDarkerColor (), attr.Background));
}
}
return base.GetNormalColor ();
}
/// <inheritdoc/>
public override void OnDrawContent (Rectangle viewport)
{
//base.OnDrawContent (viewport);
if (Orientation == Orientation.Vertical)
{
DrawVerticalShadow (viewport);
}
else
{
DrawHorizontalShadow (viewport);
}
}
private void DrawHorizontalShadow (Rectangle rectangle)
{
// Draw the start glyph
AddRune (0, 0, (Rune)HORIZONTAL_START_GLYPH);
// Fill the rest of the rectangle with the glyph
for (var i = 1; i < rectangle.Width - 1; i++)
{
AddRune (i, 0, (Rune)HORIZONTAL_GLYPH);
}
// Last is special
AddRune (rectangle.Width - 1, 0, (Rune)HORIZONTAL_END_GLYPH);
}
private void DrawVerticalShadow (Rectangle viewport)
{
// Draw the start glyph
AddRune (0, 0, (Rune)VERTICAL_START_GLYPH);
// Fill the rest of the rectangle with the glyph
for (var i = 1; i < viewport.Height; i++)
{
AddRune (0, i, (Rune)VERTICAL_GLYPH);
}
}
}

View File

@@ -59,6 +59,36 @@ public partial class View
/// </remarks>
public Margin Margin { get; private set; }
[SerializableConfigurationProperty (Scope = typeof (ThemeScope))]
public static bool DefaultShadow { get; set; } = false;
private bool _shadow;
/// <summary>
/// Gets or sets whether the View is shown with a shadow effect. The shadow is drawn on the right and bottom sides of the
/// Margin.
/// </summary>
/// <remarks>
/// Setting this property to <see langword="true"/> will add a shadow to the right and bottom sides of the Margin.
/// The View 's <see cref="Frame"/> will be expanded to include the shadow.
/// </remarks>
public bool Shadow
{
get => _shadow;
set
{
if (_shadow == value)
{
return;
}
_shadow = value;
if (Margin is { })
{
_shadow = Margin.EnableShadow (value);
}
}
}
/// <summary>
/// The <see cref="Adornment"/> that offsets the <see cref="Viewport"/> from the <see cref="Margin"/>.
/// The Border provides the space for a visual border (drawn using

View File

@@ -58,8 +58,11 @@ public class DrawEventArgs : EventArgs
public class FocusEventArgs : EventArgs
{
/// <summary>Constructs.</summary>
/// <param name="view">The view that gets or loses focus.</param>
public FocusEventArgs (View view) { View = view; }
/// <param name="leaving">The view that gets or loses focus.</param>
public FocusEventArgs (View leaving, View entering) {
Leaving = leaving;
Entering = entering;
}
/// <summary>
/// Indicates if the current focus event has already been processed and the driver should stop notifying any other
@@ -68,6 +71,10 @@ public class FocusEventArgs : EventArgs
/// </summary>
public bool Handled { get; set; }
/// <summary>Indicates the current view that gets or loses focus.</summary>
public View View { get; set; }
/// <summary>Indicates the view that is losing focus.</summary>
public View Leaving { get; set; }
/// <summary>Indicates the view that is gaining focus.</summary>
public View Entering { get; set; }
}

View File

@@ -335,14 +335,14 @@ public partial class View
if (Viewport.Contains (mouseEvent.Position))
{
if (SetHighlight (HighlightStyle.HasFlag (HighlightStyle.Pressed) ? HighlightStyle.Pressed : HighlightStyle.None) == true)
if (this is not Adornment && SetHighlight (HighlightStyle.HasFlag (HighlightStyle.Pressed) ? HighlightStyle.Pressed : HighlightStyle.None) == true)
{
return true;
}
}
else
{
if (SetHighlight (HighlightStyle.HasFlag (HighlightStyle.PressedOutside) ? HighlightStyle.PressedOutside : HighlightStyle.None) == true)
if (this is not Adornment && SetHighlight (HighlightStyle.HasFlag (HighlightStyle.PressedOutside) ? HighlightStyle.PressedOutside : HighlightStyle.None) == true)
{
return true;
@@ -533,6 +533,20 @@ public partial class View
HighlightEventArgs args = new (highlight);
Highlight?.Invoke (this, args);
if (args.Cancel)
{
return true;
}
args = new (highlight);
Margin?.Highlight?.Invoke (this, args);
//args = new (highlight);
//Border?.Highlight?.Invoke (this, args);
//args = new (highlight);
//Padding?.Highlight?.Invoke (this, args);
return args.Cancel;
}

View File

@@ -505,7 +505,7 @@ public partial class View
/// <returns><c>true</c>, if the event was handled, <c>false</c> otherwise.</returns>
public virtual bool OnEnter (View view)
{
var args = new FocusEventArgs (view);
var args = new FocusEventArgs (view, this);
Enter?.Invoke (this, args);
if (args.Handled)
@@ -521,7 +521,7 @@ public partial class View
/// <returns><c>true</c>, if the event was handled, <c>false</c> otherwise.</returns>
public virtual bool OnLeave (View view)
{
var args = new FocusEventArgs (view);
var args = new FocusEventArgs (this, view);
Leave?.Invoke (this, args);
if (args.Handled)

View File

@@ -33,6 +33,12 @@ public class Button : View
private readonly Rune _rightDefault;
private bool _isDefault;
/// <summary>
/// Gets or sets whether <see cref="Button"/>s are shown with a shadow effect by default.
/// </summary>
[SerializableConfigurationProperty (Scope = typeof (ThemeScope))]
public new static bool DefaultShadow { get; set; } = false;
/// <summary>Initializes a new instance of <see cref="Button"/>.</summary>
public Button ()
{
@@ -44,14 +50,15 @@ public class Button : View
_leftDefault = Glyphs.LeftDefaultIndicator;
_rightDefault = Glyphs.RightDefaultIndicator;
Height = Dim.Auto (DimAutoStyle.Text);
Width = Dim.Auto (DimAutoStyle.Text);
Height = Dim.Auto (DimAutoStyle.Text, minimumContentDim: 1);
CanFocus = true;
HighlightStyle |= HighlightStyle.Pressed;
#if HOVER
HighlightStyle |= HighlightStyle.Hover;
#endif
// Override default behavior of View
AddCommand (Command.HotKey, () =>
{
@@ -64,6 +71,8 @@ public class Button : View
TitleChanged += Button_TitleChanged;
MouseClick += Button_MouseClick;
Shadow = DefaultShadow;
}
private bool _wantContinuousButtonPressed;

View File

@@ -107,6 +107,10 @@ public class ColorPicker : View
get => (ColorName)_selectColorIndex;
set
{
if (value == (ColorName)_selectColorIndex)
{
return;
}
var prev = (ColorName)_selectColorIndex;
_selectColorIndex = (int)value;

View File

@@ -60,7 +60,8 @@ public class Dialog : Window
/// <remarks>
/// By default, <see cref="View.X"/>, <see cref="View.Y"/>, <see cref="View.Width"/>, and <see cref="View.Height"/> are
/// set
/// such that the <see cref="Dialog"/> will be centered in, and no larger than 90% of the screen dimensions.
/// such that the <see cref="Dialog"/> will be centered in, and no larger than 90% of <see cref="Application.Top"/>, if there is one. Otherwise,
/// it will be bound by the screen dimensions.
/// </remarks>
public Dialog ()
{

View File

@@ -14,6 +14,14 @@ namespace Terminal.Gui;
/// </remarks>
public class Window : Toplevel
{
/// <summary>
/// Gets or sets whether all <see cref="Window"/>s are shown with a shadow effect by default.
/// </summary>
[SerializableConfigurationProperty (Scope = typeof (ThemeScope))]
public static bool DefaultShadow { get; set; } = false;
/// <summary>
/// Initializes a new instance of the <see cref="Window"/> class.
/// </summary>
@@ -22,6 +30,7 @@ public class Window : Toplevel
CanFocus = true;
ColorScheme = Colors.ColorSchemes ["Base"]; // TODO: make this a theme property
BorderStyle = DefaultBorderStyle;
Shadow = DefaultShadow;
// This enables the default button to be activated by the Enter key.
AddCommand (

View File

@@ -32,6 +32,8 @@
{
"UI Catalog Theme": {
"Dialog.DefaultButtonAlignment": "Fill",
"Button.DefaultShadow": true,
"Window.DefaultShadow": true,
"ColorSchemes": [
{
"UI Catalog Scheme": {

View File

@@ -21,10 +21,9 @@ public class Adornments : Scenario
AutoSelectViewToEdit = true,
// This is for giggles, to show that the editor can be moved around.
Arrangement = ViewArrangement.Movable,
X = Pos.AnchorEnd()
X = Pos.AnchorEnd(),
};
editor.Border.Thickness = new Thickness (1, 3, 1, 1);
editor.Border.Thickness = new Thickness (1, 2, 1, 1);
app.Add (editor);
@@ -104,7 +103,7 @@ public class Adornments : Scenario
window.Padding.Add (labelInPadding);
var textFieldInPadding = new TextField
{ X = Pos.Right (labelInPadding) + 1, Y = Pos.Top (labelInPadding), Width = 15, Text = "some text" };
{ X = Pos.Right (labelInPadding) + 1, Y = Pos.Top (labelInPadding), Width = 15, Text = "some text" };
textFieldInPadding.Accept += (s, e) => MessageBox.Query (20, 7, "TextField", textFieldInPadding.Text, "Ok");
window.Padding.Add (textFieldInPadding);

View File

@@ -32,6 +32,7 @@ public class BorderEditor : AdornmentEditor
_rbBorderStyle = new RadioGroup
{
X = 0,
// BUGBUG: Hack until dimauto is working properly
Y = Pos.Bottom (Subviews [^1]),
Width = Dim.Width (Subviews [^2]) + Dim.Width (Subviews [^1]) - 1,
SelectedItem = (int)(((Border)AdornmentToEdit)?.LineStyle ?? LineStyle.None),

View File

@@ -433,6 +433,7 @@ public class Buttons : Scenario
Title = $"{CM.Glyphs.DownArrow}",
WantContinuousButtonPressed = true,
CanFocus = false,
Shadow = false,
};
_number = new ()
@@ -457,6 +458,7 @@ public class Buttons : Scenario
Title = $"{CM.Glyphs.UpArrow}",
WantContinuousButtonPressed = true,
CanFocus = false,
Shadow = false,
};
CanFocus = true;

View File

@@ -36,6 +36,7 @@ public class ExpanderButton : Button
Height = 1;
NoDecorations = true;
NoPadding = true;
Shadow = false;
AddCommand (Command.HotKey, Toggle);
AddCommand (Command.ToggleExpandCollapse, Toggle);

View File

@@ -13,6 +13,22 @@ public class MarginEditor : AdornmentEditor
private void MarginEditor_Initialized (object sender, EventArgs e)
{
var ckbShadow = new CheckBox
{
X = 0,
//Y = Pos.AnchorEnd(),
// BUGBUG: Hack until dimauto is working properly
Y = Pos.Bottom (Subviews [^1]),
SuperViewRendersLineCanvas = true,
Title = "_Shadow",
Enabled = AdornmentToEdit is { },
};
ckbShadow.Toggled += (sender, args) =>
{
((Margin)AdornmentToEdit).EnableShadow (args.NewValue!.Value);
};
Add (ckbShadow);
}
}

View File

@@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.Metrics;
using System.Linq;
using Terminal.Gui;
namespace UICatalog.Scenarios;
[ScenarioMetadata ("3D Effects Demo", "Demonstrates 3D UI Effects.")]
[ScenarioCategory ("Layout")]
public class ThreeD : Scenario
{
public override void Main ()
{
Application.Init ();
Window app = new ()
{
Title = $"{Application.QuitKey} to Quit - Scenario: {GetName ()}"
};
var editor = new AdornmentsEditor ()
{
AutoSelectViewToEdit = true,
};
app.Add (editor);
Window win = new ()
{
X = Pos.Right (editor),
Y = 0,
Width = Dim.Percent (30),
Height = Dim.Percent (30),
Title = "Shadow Window",
Arrangement = ViewArrangement.Movable,
};
var buttonInWin = new Button
{
X = Pos.Center (),
Y = Pos.Center (), Text = "Button in Window",
//Shadow = true
};
win.Add (buttonInWin);
app.Add (win);
var button = new Button
{
X = Pos.Right (editor) + 10,
Y = Pos.Center (), Text = "Button",
};
app.Add (button);
Application.Run (app);
app.Dispose ();
Application.Shutdown ();
return;
}
}

View File

@@ -330,7 +330,6 @@ internal class UICatalogApp
// made by Scenario.Init() above
// TODO: Throw if shutdown was not called already
Application.Shutdown ();
VerifyObjectsWereDisposed ();
}
@@ -388,6 +387,8 @@ internal class UICatalogApp
public UICatalogTopLevel ()
{
_diagnosticFlags = View.Diagnostics;
_themeMenuItems = CreateThemeMenuItems ();
_themeMenuBarItem = new ("_Themes", _themeMenuItems);

View File

@@ -14,7 +14,7 @@ public class NeedsDisplayTests ()
view.BeginInit();
view.EndInit();
Assert.False (view.NeedsDisplay);
Assert.False (view.SubViewNeedsDisplay);
//Assert.False (view.SubViewNeedsDisplay);
}

View File

@@ -247,7 +247,7 @@ This TextFormatter (tf2) with fill will be cleared on rewritten. ",
label.Text = "This label is rewritten.";
Assert.True (label.NeedsDisplay);
Assert.True (label.LayoutNeeded);
Assert.False (label.SubViewNeedsDisplay);
//Assert.False (label.SubViewNeedsDisplay);
label.Draw ();
tf1.Text = "This TextFormatter (tf1) is rewritten.";

View File

@@ -1167,12 +1167,12 @@ public class ToplevelTests (ITestOutputHelper output)
if (iterations == 1)
{
steps [0] = iterations;
Assert.Null (e.View);
Assert.Null (e.Leaving);
}
else
{
steps [3] = iterations;
Assert.Equal (diag, e.View);
Assert.Equal (diag, e.Leaving);
}
};
@@ -1181,7 +1181,7 @@ public class ToplevelTests (ITestOutputHelper output)
// This will never be raised
iterations++;
isLeaveTop = true;
Assert.Equal (diag, e.View);
Assert.Equal (diag, e.Leaving);
};
top.Add (vt);
@@ -1207,7 +1207,7 @@ public class ToplevelTests (ITestOutputHelper output)
iterations++;
steps [1] = iterations;
isEnterDiag = true;
Assert.Null (e.View);
Assert.Null (e.Leaving);
};
vd.Leave += (s, e) =>
@@ -1215,7 +1215,7 @@ public class ToplevelTests (ITestOutputHelper output)
iterations++;
steps [2] = iterations;
isLeaveDiag = true;
Assert.Equal (top, e.View);
Assert.Equal (top, e.Entering);
};
diag.Add (vd);