gui-cs / Terminal.Gui

Cross Platform Terminal UI toolkit for .NET
MIT License
9.73k stars 694 forks source link

Add 3D Shadow Effect for `Button` and other views #2144

Closed heinrich-ulbricht closed 5 months ago

heinrich-ulbricht commented 2 years ago

Is your feature request related to a problem? Please describe. Nope, just feature request I guess

Describe the solution you'd like I'd like to add buttons like Norton Commander used back in the day. They had a half-height shadow at the bottom. But currently with Terminal.GUI it only seems to be possible to add a full-height shadow at the bottom. This is kind of fat for a small button. The shadow also needs to be set off vertically only half-height (or is it like 1/3?) on the right.

Here's what it should look like:

image

I looked at all the border samples in UICatalog but did not see such a feature. It might be impossible. Or is there some rune to configure? I'm not so deep in the code.

I'm currently playing around with color schemes and the 3D effect - not very shadowy for a small button: image

Describe alternatives you've considered Using no shadow for buttons.

tznind commented 2 years ago

I agree that this would be a great core feature for the library. But in the mean time here is what you can do with the current release and overriding the Button redraw method.

image

using Terminal.Gui;

Application.Init();
var win = new Window("Example App (Ctrl+Q to quit)");

win.Add(new ShadowButton("Button 1"){
    X = 2,
    Y = 1
});

win.Add(new ShadowButton("Button 2")
{
    X = 20,
    Y = 1
});

Application.Run(win);
Application.Shutdown();

class ShadowButton : Button
{
    public ShadowButton(string text):base(text + " ") /*put a space on to leave 'shadow' room*/
    {
        Height = 2;
    }
    public override void Redraw(Rect bounds)
    {
        // draw regular button
        base.Redraw(bounds);

        // draw the 'end' button symbol one in
        AddRune(bounds.Width - 2,0, ']');

        // shadow color
        Driver.SetAttribute(new Terminal.Gui.Attribute(Color.Black, 
                Colors.Base.Normal.Background));

        // end shadow (right)
        AddRune(bounds.Width - 1, 0, '▄');

        // leave whitespace in lower left in parent/default background color
        Driver.SetAttribute(new Terminal.Gui.Attribute(Color.Black, 
                Colors.Base.Normal.Background));
        AddRune(0, 1, ' ');

        // The color for rendering shadow is 'black' + parent/default background color
        Driver.SetAttribute(new Terminal.Gui.Attribute(Colors.Base.Normal.Background,
                Color.Black));

        // underline shadow                
        for (int x = 1;x<bounds.Width;x++)
        {
            AddRune(x,1, '▄');
        }       
    }
}

UPDATE: From a little quick experimenting the 'three quarter block' renders as a question mark in PowerShell (out of the box windows 10). Only the 'half blocks' are there default. Same applies to some of the other fancier 'block' sizes.

Theres also a little 'bleed' on the right of the button where 1 pixel of terminal is black. Don't know if that is a font thing or a bug in powershell/visual studio console.

tznind commented 2 years ago

For reference these are the blocks available (not all of which are supported by all terminals)

Block elements 2580 ▀ UPPER HALF BLOCK 2581 ▁ LOWER ONE EIGHTH BLOCK 2582 ▂ LOWER ONE QUARTER BLOCK 2583 ▃ LOWER THREE EIGHTHS BLOCK 2584 ▄ LOWER HALF BLOCK 2585 ▅ LOWER FIVE EIGHTHS BLOCK 2586 ▆ LOWER THREE QUARTERS BLOCK 2587 ▇ LOWER SEVEN EIGHTHS BLOCK 2588 █ FULL BLOCK = solid → 25A0 ■ black square 2589 ▉ LEFT SEVEN EIGHTHS BLOCK 258A ▊ LEFT THREE QUARTERS BLOCK 258B ▋ LEFT FIVE EIGHTHS BLOCK 258C ▌ LEFT HALF BLOCK 258D ▍ LEFT THREE EIGHTHS BLOCK 258E ▎ LEFT ONE QUARTER BLOCK 258F ▏ LEFT ONE EIGHTH BLOCK 2590 ▐ RIGHT HALF BLOCK Shade characters 2591 ░ LIGHT SHADE • 25% 2592 ▒ MEDIUM SHADE = speckles fill, dotted fill • 50% • used in mapping to cp949 → 1FB90 🮐 inverse medium shade 2593 ▓ DARK SHADE • 75% Block elements 2594 ▔ UPPER ONE EIGHTH BLOCK 2595 ▕ RIGHT ONE EIGHTH BLOCK Terminal graphic characters 2596 ▖ QUADRANT LOWER LEFT 2597 ▗ QUADRANT LOWER RIGHT 2598 ▘ QUADRANT UPPER LEFT 2599 ▙ QUADRANT UPPER LEFT AND LOWER LEFT AND LOWER RIGHT 259A ▚ QUADRANT UPPER LEFT AND LOWER RIGHT → 1F67F 🙿 reverse checker board → 1FB95 🮕 checker board fill 259B ▛ QUADRANT UPPER LEFT AND UPPER RIGHT AND LOWER LEFT 259C ▜ QUADRANT UPPER LEFT AND UPPER RIGHT AND LOWER RIGHT 259D ▝ QUADRANT UPPER RIGHT 259E ▞ QUADRANT UPPER RIGHT AND LOWER LEFT → 1F67E 🙾 checker board → 1FB96 🮖 inverse checker board fill 259F ▟ QUADRANT UPPER RIGHT AND LOWER LEFT AND LOWER RIGHT

http://www.unicode.org/charts/PDF/U2580.pdf

BDisp commented 2 years ago

I think setting the Border property with the Effect3D enabled on Button will do the trick.

tznind commented 2 years ago

I think setting the Border property with the Effect3D enabled on Button will do the trick.

Effect3D uses a full border element and only has it below the Button.

Button3 uses the Effect3D while Button1 and Button2 use the ShadowButton implementation. Both look ok but I think the half box rendering looks nicer. But there are definetly issues:

Also the ShadowButton click area includes the shadow (clicks in shadow will focus/click the button) which is different... not sure if better or worse but definetly different.

image

win.Add(new Button("Button 3")
{
    X = 40,
    Y = 1,
    Border = new Border
    {
        Effect3D = true,
        Effect3DBrush = new Attribute(Color.Black,Color.Black),
    }
});

Heres what the shadows look like when theres a background that isn't just solid color. You can see the eye expects to see half an 'x' where that half shadow ends.

image

tznind commented 2 years ago

I have to say the Norton Commander query box in OP does look awesome. I'm getting inspired to ressurect this failed PR in the Designer https://github.com/gui-cs/TerminalGuiDesigner/pull/111 I like the grey/white and yellow/red (focus).

tig commented 2 years ago

I agree the Norton style looks great.

I'd love to see us tackle a new visual style as part of v2. See https://github.com/gui-cs/Terminal.Gui/discussions/1940

heinrich-ulbricht commented 2 years ago

The ShadowButton already looks pretty good! If just the 1 pixel black line wasn't there 😑

BDisp commented 2 years ago

You can see the eye expects to see half an 'x' where that half shadow ends.

I think printing half of a letter or digit on a terminal is impossible.

tznind commented 2 years ago

You can see the eye expects to see half an 'x' where that half shadow ends.

I think printing half of a letter or digit on a terminal is impossible.

Yeah haha sorry I didn't mean it as a solution. Was just weighing up the pros and cons of each of them. The half height shadow looks wierd when hovering over background stuff but theres not much that can be done about it.

I guess if we add it we should make it feature toggle. Thick or thin shadows. As theres not likely to always be a clear 'best'

tznind commented 2 years ago

The ShadowButton already looks pretty good! If just the 1 pixel black line wasn't there 😑

Its so wierd! when you zoom in it doesn't get any bigger and it's there on cmd, powershell and visual studio dev terminal. I'm going to test this on my linux machine asap - see if it is something about my desktop resolution or something.

image When zoomed in the bleed is still single pixel

tznind commented 2 years ago

Yup, as I thought. Ubuntu with Terminator does not have this artifact, neither does Terminology console.

nobleed

Is anyone please able to test to see if this line artifact appears for them on Windows? I'm hoping this is a hardware issue.

Source code is in my initial reply.

BDisp commented 2 years ago

I confirm that doesn't appears on "Windows Terminal". Only with Windows Host Console that line artifact appears.

heinrich-ulbricht commented 2 years ago

Windows 11 Console Window Host 😞 - the artifact is there. image

Console in 2022 starts to seem like rocket science.

UX-related: when clicking the button I felt that I expected it to go down when pressed.

BDisp commented 2 years ago

UX-related: when clicking the button I felt that I expected it to go down when pressed.

It only go down on click and not on button pressed. So if you press the mouse button and move it before released, the button click will not be fired.

tznind commented 2 years ago

How about this? Works with keyboard or mouse because it responds to the Click event not the mouse down.

pushbuttons

I think it looks a little off since it drops lower than the shadow indicates but I think that is the best your going to get with the limitations of console column height/resolution. Making the shadow thicker will make the buttons look worse.

Its also going to be tough for the 'depressed' button to reveal any background View content (at the moment it draws ' ' over the revealed area).

Heres the code:

using Terminal.Gui;
using Attribute = Terminal.Gui.Attribute;

Application.Init();
var win = new Window("Example App (Ctrl+Q to quit)");

win.Add(new ShadowButton("Button 1")
{
    X = 2,
    Y = 1
});

win.Add(new ShadowButton("Button 2")
{
    X = 20,
    Y = 1
});

win.Add(new Button("Button 3")
{
    X = 40,
    Y = 1,
    Border = new Border
    {
        Effect3D = true,
        Effect3DBrush = new Attribute(Color.Black, Color.Black),
    }
});

Application.Run(win);
Application.Shutdown();

class ShadowButton : Button
{
    public ShadowButton(string text) : base(text + " ") /*put a space on to leave 'shadow' room*/
    {
        Height = 2;
    }

    public bool Depressed { get; private set; }
    public override void OnClicked()
    {
        base.OnClicked();

        Depressed = true;
        SetNeedsDisplay();

        Application.MainLoop.AddTimeout(TimeSpan.FromMilliseconds(500), (m) =>
        {
            Depressed = false;
            Application.MainLoop.Invoke(() =>
            {
                SetNeedsDisplay();
            });
            return false;
        });

    }

    public override void Redraw(Rect bounds)
    {
        if (Depressed)
        {
            DrawDepressed(bounds);
        }
        else
        {
            DrawRegular(bounds);
        }
    }

    private void DrawRegular(Rect bounds)
    {
        // draw regular button
        base.Redraw(bounds);

        // draw the 'end' button symbol one in
        AddRune(bounds.Width - 2, 0, ']');

        // shadow color
        Driver.SetAttribute(new Terminal.Gui.Attribute(Color.Black, Colors.Base.Normal.Background));

        // end shadow (right)
        AddRune(bounds.Width - 1, 0, '▄');

        Driver.SetAttribute(new Terminal.Gui.Attribute(Color.Black, Colors.Base.Normal.Background));
        AddRune(0, 1, ' ');

        Driver.SetAttribute(new Terminal.Gui.Attribute(Colors.Base.Normal.Background, Color.Black));

        // underline shadow                
        for (int x = 1; x < bounds.Width; x++)
        {
            AddRune(x, 1, '▄');
        }
    }

    private void DrawDepressed(Rect bounds)
    {
        // area revealed on the background control by pushing Button down
        Driver.SetAttribute(
            new Terminal.Gui.Attribute(Colors.Base.Normal.Foreground, 
                Colors.Base.Normal.Background));

        // clear top line so button sinks to bottom line
        for (int x = 0; x < bounds.Width; x++)
        {
            AddRune(x,0,' ');
        }

        // render button as down
        Driver.SetAttribute(
            HasFocus ?
            new Terminal.Gui.Attribute(ColorScheme.Focus.Foreground,
                Colors.Base.Focus.Background):
            new Terminal.Gui.Attribute(ColorScheme.Normal.Foreground,
                Colors.Base.Normal.Background));

        var buttonRenderText = TextFormatter.Text;

        var textWidth = buttonRenderText.Length;

        for (int x = 1; x < textWidth; x++)
        {
            AddRune(x, 1, buttonRenderText[x-1]);
        }

        AddRune(textWidth-1, 1, ']');

    }
}
BDisp commented 2 years ago

UX-related: when clicking the button I felt that I expected it to go down when pressed.

It only go down on click and not on button pressed. So if you press the mouse button and move it before released, the button click will not be fired.

Ah ok sorry, you were meaning about visually effect.

heinrich-ulbricht commented 2 years ago

While googling for Norton Commander button animations I came across this repo: https://github.com/magiblot/tvision - there is a screenshot of a button also showing those vertical lines in the button shadow, but between the shadow blocks. On purpose? Maybe another block type?

image
tznind commented 2 years ago

I have sometimes seen those appear when resizing or moving terminal around. I think we shouldn't worry about these artifacts (bleed, seperation lines etc). They are pretty minor and are not really our responsibility to resolve.

BDisp commented 2 years ago

I have sometimes seen those appear when resizing or moving terminal around. I think we shouldn't worry about these artifacts (bleed, seperation lines etc). They are pretty minor and are not really our responsibility to resolve.

You are right. With Windows Terminal on Windows 11 that doesn't happens and probably M$ will not worry with that on previous versions.

heinrich-ulbricht commented 2 years ago

Sounds reasonable. I'm nevertheless curious how the actual animation looks in either Turbo Vision or Norton Commander. They must have had the same challenges. Couldn't find something so far.

BDisp commented 2 years ago

@tznind and how about only hide the shadows when the button go down, instead of redraw the button bellow? How will be the effect?

heinrich-ulbricht commented 2 years ago

@BDisp Your suggestion looks pretty nice. The button still moves one to the right, giving good visual feedback:

https://user-images.githubusercontent.com/3469970/199702244-b564db88-5661-44f6-8ea4-40886522c7bd.mp4

(The gray text appears because I disable the button when being clicked. This might require more tuning with regard to timing.) And the button does not retain its focus color, yet. It flickers, then it's gone. Focus is still on the button, though.

    class ShadowButton : Button
    {
        public ShadowButton(string text) : base(text + " ") /*put a space on to leave 'shadow' room*/
        {
            Height = 2;
        }

        public bool Depressed { get; private set; }
        public override void OnClicked()
        {
            base.OnClicked();

            Depressed = true;
            SetNeedsDisplay();

            Application.MainLoop.AddTimeout(TimeSpan.FromMilliseconds(180), (m) =>
            {
                Depressed = false;
                Application.MainLoop.Invoke(() =>
                {
                    SetNeedsDisplay();
                });
                return false;
            });

        }

        public override void Redraw(Rect bounds)
        {
            if (Depressed)
            {
                DrawDepressed(bounds);
            }
            else
            {
                DrawRegular(bounds);
            }
        }

        private void DrawRegular(Rect bounds)
        {
            // draw regular button
            base.Redraw(bounds);

            // draw the 'end' button symbol one in
            AddRune(bounds.Width - 2, 0, ']');

            // shadow color
            Driver.SetAttribute(new Terminal.Gui.Attribute(Color.Black, Colors.Base.Normal.Background));

            // end shadow (right)
            AddRune(bounds.Width - 1, 0, '▄');

            Driver.SetAttribute(new Terminal.Gui.Attribute(Color.Black, Colors.Base.Normal.Background));
            AddRune(0, 1, ' ');

            Driver.SetAttribute(new Terminal.Gui.Attribute(Colors.Base.Normal.Background, Color.Black));

            // underline shadow                
            for (int x = 1; x < bounds.Width; x++)
            {
                AddRune(x, 1, '▄');
            }
        }

        private void DrawDepressed(Rect bounds)
        {
            // area revealed on the background control by pushing Button down
            Driver.SetAttribute(
                new Terminal.Gui.Attribute(Colors.Base.Normal.Foreground,
                    Colors.Base.Normal.Background));

            // clear bottom line (shadow)
            for (int x = 0; x < bounds.Width; x++)
            {
                AddRune(x, 2, ' ');
            }

            // render button as down
            Driver.SetAttribute(
                HasFocus ?
                new Terminal.Gui.Attribute(ColorScheme.Focus.Foreground,
                    Colors.Base.Focus.Background) :
                new Terminal.Gui.Attribute(ColorScheme.Normal.Foreground,
                    Colors.Base.Normal.Background));

            var buttonRenderText = TextFormatter.Text;

            var textWidth = buttonRenderText.Length;

            for (int x = 1; x < textWidth; x++)
            {
                AddRune(x, 0, buttonRenderText[x - 1]);
            }

            AddRune(textWidth - 1, 0, ']');

        }
    }
BDisp commented 2 years ago

Thanks. I almost have an half border implementation in the Border class when the height is equal to 1. I think there is no sense to make it if the width is equal to 1. Let's me know.

tznind commented 2 years ago

Very nice effect @heinrich-ulbricht . Definetly better than jumping down a line 🎉

heinrich-ulbricht commented 2 years ago

@tznind Cannot get enough of it :D

https://user-images.githubusercontent.com/3469970/199712014-fb4e134d-7baa-49a7-9726-69b998ce1600.mp4

BDisp commented 2 years ago

I only removed the shadow but this effects is better. I'm leverage the existing Border class to make it more reused to another views. So, this additional move must be implemented in each view that want to implement it.

heinrich-ulbricht commented 2 years ago

Unfortunately the focus color seems lost after selecting the button. Is this expected in this proof of concept stage or should it be there? AH nevermind. It only happens when disabling/enabling the button during the animation. Works great if not disabling as can be seen in the last video. (But is it normal...? See first video...) Nevermind 2: this seems to be normal button behavior, nothing to do with the animation.

tznind commented 2 years ago

Just thought I'd mention that we can do this without subclassing using the DrawContentComplete event. That is what I have done in designer (but not the animation yet). The main reason I did it was so could have the View work in TerminalGuiDesigner (doesn't currently support user subclass views) but also because avoiding inheritence is often a good choice when its for subtle stuff.

https://github.com/gui-cs/TerminalGuiDesigner/blob/60e853d57acb633e47e65867274a97221f47d607/src/UI/Windows/ConfirmDialog.cs#L36-L42

I know @BDisp is creating a border style that will do most of the drawing work in this.

Hopefullly after that, we can create a simple 5/10 line class ShadowStyler that registers events and adjusts borders to achieve this in a much simpler way?

heinrich-ulbricht commented 2 years ago

Note: removing the start and end rune from the 3D button seems to reduce the visual noise a bit. Experimenting how it feels.

tig commented 2 years ago

Question for y'all:

Is it realistic to think this could be done/tested before next Tuesday for inclusion in v1.9.0 (https://github.com/gui-cs/Terminal.Gui/milestone/5)?

tznind commented 2 years ago

Question for y'all:

Is it realistic to think this could be done/tested before next Tuesday for inclusion in v1.9.0 (https://github.com/gui-cs/Terminal.Gui/milestone/5)?

I think it depends on @BDisp availability as his PR #2166 is adding the functionality. I think if we can squeeze it in that would be great as its a great 'killer feature' for the patch.

BDisp commented 2 years ago

Question for y'all:

Is it realistic to think this could be done/tested before next Tuesday for inclusion in v1.9.0 (https://github.com/gui-cs/Terminal.Gui/milestone/5)?

I'll try to add at least the functionality to work on whatever border size, but only using the current glyphs, ok?

BDisp commented 2 years ago

The 2588 █ FULL BLOCK isn't print, but it print in the Character Map. @tznind can you test please. Thanks.

heinrich-ulbricht commented 1 year ago

@tig Greetings! You removed this from the v2.0 milestone - is this shadow-for-button feature being scrapped or just included in another feature?

BDisp commented 1 year ago

@heinrich-ulbricht with the new Frame class which the View class now have 3 Frame's for Margin, Border and Padding, it's now possible manipulating his Thickness and ColorScheme properties to make the shadow effect.

heinrich-ulbricht commented 1 year ago

@BDisp Wonderful ❤️

tznind commented 1 year ago

I'm interested in how it will work in the new API so I had an experiment. You can do this:

Application.Init();
var w = new Window();
var btn = new Button("Click me"){
    X = 2,
    Y = 2
};

btn.Margin.Thickness = new Thickness{
    Bottom = 1,
    Right = 1,
};
btn.Margin.ColorScheme = new ColorScheme(){
    Normal = new Attribute(Color.Black)
};

w.Add(btn);
Application.Run(w);
Application.Shutdown();

But if you want half height effect you need to do a bit more work in Draw. I did try adding draw code to Margin draw events but I don't think they are hooked up yet.

btn.DrawContentComplete += (s,e)=>
{
    Application.Driver.SetAttribute(
        new Attribute(
            Color.Black,
            w.ColorScheme.Normal.Background
            ));

    for(int x = 0 ; x <= btn.Margin.Bounds.Width ;x++)
        btn.AddRune(x,1,'▀');
};

shot-2023-05-16_22-47-14

tig commented 6 months ago

I am addressing this issue in

heinrich-ulbricht commented 6 months ago

@tig Looking at my older videos above I notice that in those the button just slightly moves to the right, but stays in the same "row", not moving down. At the same time the shadow disappears. Overall this gives the illusion of movement.

You chose a different approach and the button moves a "row" down to where the shadow was. Is this a deliberate choice (because it looks better in your eyes), or is there a technical necessity?

For my eye it looks more pleasing if the button stays right where the mouse is and doesn't move a "row" down, although this might not be physically correct. Somehow with your approach I expect the mouse cursor to stick to the button and move as well - as it would be when pressing a button with the finger. Hope it's clear what I mean :D

tig commented 6 months ago

@tig Looking at my older videos above I notice that in those the button just slightly moves to the right, but stays in the same "row", not moving down. At the same time the shadow disappears. Overall this gives the illusion of movement.

You chose a different approach and the button moves a "row" down to where the shadow was. Is this a deliberate choice (because it looks better in your eyes), or is there a technical necessity?

For my eye it looks more pleasing if the button stays right where the mouse is and doesn't move a "row" down, although this might not be physically correct. Somehow with your approach I expect the mouse cursor to stick to the button and move as well - as it would be when pressing a button with the finger. Hope it's clear what I mean :D

Much better! Thanks for pointing that out.

7o1Zd6L 1