Skip to content

Conversation

@Vinci10
Copy link
Contributor

@Vinci10 Vinci10 commented Jul 4, 2025

Description:

(Based on #5830)
Fixes:

  • #4129 (shadows only)
  • #4411 (shadows on the right and bottom can be added using proper offset)

Shadow Support for Rectangle, Square, and Circle

  • Introduced a Shadow struct and Shadowable interface to provide reusable shadow logic.

  • Added fields for shadow color, softness, offset, and type (drop/box shadow).

  • The ExpandForShadow field allows the object to keep its requested/original size, expanding the total bounds to include the shadow.

  • Updated shader and painter logic to render shadows with configurable softness and offset, and to blend shadows correctly.

  • If a canvas object has a shadow added, the canvas size is reduced by the shadow paddings on each side. This ensures that the entire object—including its shadow—fits within the originally requested frame size. The content area (without shadow) will be smaller to accommodate the shadow within the allocated space. If a canvas object has a shadow added, the canvas coords are expanded to accommodate the shadow.

  • Shader and Painter Updates

    • Rectangle and round-rectangle shaders updated to support shadow rendering.
    • Painter logic updated to pass new parameters and handle shadow bounds.
  • API Additions

    • New methods and fields are documented in code comments.
    • Backwards compatibility is maintained for existing code.

Status

  • This is a draft PR.
  • I am looking for feedback and suggestions from the Fyne team regarding:
    • API design and naming
    • Performance considerations
  • Tests are not yet included.
  • Performance has not been fully evaluated.

Additional Notes

This work is a prerequisite for adding shadows and rounded corners to popups, dialogs, and menus in Fyne. I have already applied these changes to those components and have working code locally. Rectangle shadow provides the necessary foundation for theme-level enhancements.

Examples

shadow_examples

Dialog, menu shadow (preview)

dialog_shadow
menu_shadow


Please let me know if you have any suggestions, concerns, or requests for changes.

Checklist:

  • Tests included.
  • Lint and formatter run with no errors.
  • Tests all pass.

Where applicable:

  • Public APIs match existing style and have Since: line.
  • Any breaking changes have a deprecation path or have been discussed.
  • Check for binary size increases when importing new modules.

@Vinci10
Copy link
Contributor Author

Vinci10 commented Jul 4, 2025

By default, adding a shadow currently impacts the geometry: the canvas size is reduced by the shadow paddings so that both the object and its shadow fit inside the requested frame. This means the content (e.g., a rectangle) is shrunk to ensure the shadow does not overflow the frame.

To address this, I introduced the ExpandForShadow option. When set to true, the object keeps its requested/original size, and the overall canvas size is increased to fit the shadow outside the content.

Example Calculations

Example 1

Suppose you have a rectangle with a requested frame size of 150 x 250.

  • Shadow parameters:
    • Offset: (-10, 15)
    • Softness: 8

Shadow paddings calculation:

  • Left: offsetX + softness = -10 + 8 = -2 (clamped to 0)
  • Top: -offsetY + softness = -15 + 8 = -7 (clamped to 0)
  • Right: -offsetX + softness = 10 + 8 = 18
  • Bottom: offsetY + softness = 15 + 8 = 23

After clamping negatives to zero:

  • Left: 0
  • Top: 0
  • Right: 18
  • Bottom: 23
Mode Rectangle Size Canvas Size Rectangle Position Canvas Position
No shadow 150 x 250 150 x 250 (0, 0) (0, 0)
Shadow, ExpandForShadow=false 132 x 227 150 x 250 (0, 0) (0, 0)
Shadow, ExpandForShadow=true 150 x 250 168 x 273 (0, 0) (0, 0)

Example 2

Suppose you have a rectangle with a requested frame size of 100 x 100.

  • Shadow parameters:
    • Offset: (12, -6)
    • Softness: 5

Shadow paddings calculation:

  • Left: 12 + 5 = 17
  • Top: 6 + 5 = 11
  • Right: -12 + 5 = -7 (clamped to 0)
  • Bottom: -6 + 5 = -1 (clamped to 0)

After clamping negatives to zero:

  • Left: 17
  • Top: 11
  • Right: 0
  • Bottom: 0
Mode Rectangle Size Canvas Size Rectangle Position Canvas Position
No shadow 100 x 100 100 x 100 (0, 0) (0, 0)
Shadow, ExpandForShadow=false 83 x 89 100 x 100 (17, 11) (0, 0)
Shadow, ExpandForShadow=true 100 x 100 117 x 111 (0, 0) (-17, -11)

Notes:

  • When ExpandForShadow is false, the rectangle is shrunk and shifted inside the canvas to fit the shadow, so the rectangle's position changes (by the left/top padding), but the canvas position remains the same.
  • When ExpandForShadow is true, the rectangle keeps its original position and size, but the canvas itself is shifted (by the left/top padding) to ensure the rectangle appears at the requested position, and the overall canvas size is increased to fit

This approach allows developers to choose whether the shadow should be included inside the original frame (shrinking the content) or expand the overall bounds to preserve the content size.

Please let me know if you’d like further clarification or adjustments to this behavior!

@andydotxyz
Copy link
Member

Copying from the wider discussion as it pertains to the API design here.

You realise that an object is not restricted to drawing in its "frame"?

I don't think we should add more APIs to control how the geometry will be interpreted, it will just lead to confusion.
Simple and standard is the way - provide the "path of least surprise".

I don't think the presence of a shadow should change how the geometry of a widget or shape is interpreted at all. With the maths illustrations above complexity goes up substantially and widgets with shadows will no longer line up with others which breaks the simplicity of our layout model.

@andydotxyz
Copy link
Member

#4411 (shadows on the right and bottom can be added using proper offset)

Just FYI on that one, the bug is not about the drop of the shadow or its depth - the report is that a menu popping up from a menu bar should be at the same level so there should be shadow on 3 sides of the menu and the underside of the bar, but not between them (top of the menu)

// with the input size. Total size is larger than the input size.
SizeAndPositionWithShadow(size fyne.Size) (fyne.Size, fyne.Position)
// ShadowPaddings returns the paddings (left, top, right, bottom) for the shadow.
ShadowPaddings() [4]float32
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like the only one of the interface methods that adds (rather than works around geometry complications).

However I wonder if ShadowOffset and ShadowDepth really are all we need to draw shadows around objects - all these calculations could be removed and made a rendering only complexity.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So all calculations should be moved to painter draw.go file ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The interface has been updated. Now only the ShadowPaddings method is available

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. As noted on the recent review now that it is reduced to padding calculations it seems not needed at all...

type baseShadow struct {
baseObject

ShadowColor color.Color // Color of the shadow.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One API consideration for this PR should be:

  • Is the shadow a themed colour or a canvas feature

Which impacts how it should be implemented. If widgets are to have shadows it must be in the theme - but for canvas objects you get lower level control. Worth considering in the bigger picture how this replaces the existing (gradient) shadow implementation.

We should replace old with new in the scope of this PR rather than having two different shadow implementations - plus it will inform the correct API.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about gradient shadow implementation if only one-side shadow is needed like for scroll ? I think it still may be useful, but I could be wrong

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes a fair point - but for the major shadow usages (where it is a complete shadow) we should replace and test

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I replaced ShadowingRenderer and updated tests. I started with some initial values for shadow parameters which should be adjusted to make shadow looking good. I still need to update macos and mobile tests. I can start fixing tests once final shadow parameters per object (dialogs, widgets, etc.) are determined.

@andydotxyz
Copy link
Member

Very cool indeed thanks. I have made a few API /design comments inline.

Another thing to note would be that we have also the software renderer which would need to understand this new feature and draw something similar for compatibility.
This would also open up easier unit testing.

@Vinci10
Copy link
Contributor Author

Vinci10 commented Jul 4, 2025

#4411 (shadows on the right and bottom can be added using proper offset)

Just FYI on that one, the bug is not about the drop of the shadow or its depth - the report is that a menu popping up from a menu bar should be at the same level so there should be shadow on 3 sides of the menu and the underside of the bar, but not between them (top of the menu)

You're right, the top shadow is the problem there. I just wanted to point out that a rectangular shadow may help with that.

Copying from the wider discussion as it pertains to the API design here.

You realise that an object is not restricted to drawing in its "frame"?

I don't think we should add more APIs to control how the geometry will be interpreted, it will just lead to confusion.
Simple and standard is the way - provide the "path of least surprise".

I don't think the presence of a shadow should change how the geometry of a widget or shape is interpreted at all. With the maths illustrations above complexity goes up substantially and widgets with shadows will no longer line up with others which breaks the simplicity of our layout model.

I also prefer simple solutions :) To be honest, I am not an expert in such graphical implementations. The biggest challenge was to draw the shadow correctly and that's what I focused all my attention on. However, all changes in geometry were made only to "see" the desired effect, because without any change the "frame" clipped the shadow and I was not able to assess whether it was drawing correctly. That's why I am open to any suggestions regarding the API and changes on the library side.

@andydotxyz
Copy link
Member

because without any change the "frame" clipped the shadow

This will be a limitation of the rendering and not part of the Fyne geometry design. If you look at rectangle, line and others you will see a vectorPad value which is how we expand the frame to draw outside of the original rectangle if required. Expanding that to accommodate your new shadow should fix the issue and all the geometry changes can be reverted.

@Vinci10
Copy link
Contributor Author

Vinci10 commented Jul 4, 2025

because without any change the "frame" clipped the shadow

This will be a limitation of the rendering and not part of the Fyne geometry design. If you look at rectangle, line and others you will see a vectorPad value which is how we expand the frame to draw outside of the original rectangle if required. Expanding that to accommodate your new shadow should fix the issue and all the geometry changes can be reverted.

That would be great, so I can just extend vectorPad by shadow paddings and it would work? I will test that approach

I don't fully understand how Fyne's design geometry works, and that's why I added so much unnecessary code.

@andydotxyz
Copy link
Member

That would be great, so I can just extend vectorPad by shadow paddings and it would work? I will test that approach

Yes

I don't fully understand how Fyne's design geometry works, and that's why I added so much unnecessary code.

In essence it is really simple - a device agnostic coordinate system so it's consistent across all devices. Every item is positioned relative to the parent. More info at https://docs.fyne.io/architecture/geometry

@Vinci10
Copy link
Contributor Author

Vinci10 commented Jul 16, 2025

I reverted geometry changes. As of now shadow paddings expand coords in vecRectCoordsWithPad method. Please let me know if that is correct.

Result:
shadow

@Vinci10
Copy link
Contributor Author

Vinci10 commented Jul 23, 2025

I added shadow to software painter along with test cases

@andydotxyz
Copy link
Member

I reverted geometry changes. As of now shadow paddings expand coords in vecRectCoordsWithPad method. Please let me know if that is correct.

Thanks, I appreciate that when packed together it looks peculiar but I think this is better for the geometry.
It does raise the question of whether shadows should be drawn below all other items, but I think that is maybe not needed and maybe for another time.

@coveralls
Copy link

Coverage Status

coverage: 62.491% (+0.1%) from 62.391%
when pulling 8de9717 on Vinci10:rectangle_shadow
into 170ef1d on fyne-io:develop.

Copy link
Member

@andydotxyz andydotxyz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working more on this, it is coming together.

I have a few questions at this time:

  1. Isn't the geometry in the software painter wrong? The objects are moving out of their position to accommodate the shadow
  2. The Shadowable interface isn't clear - what is its purpose? It doesn't seem to be needed
  3. The public Shadow type can only be used as embedded in other types - what are your thoughts on this balance?

if shadowColor != color.Transparent && shadowColor != nil && (!shadowOffset.IsZero() || shadowSoftness > 0.0) {
bounds = drawShadow(c, obj, fyne.NewSize(width, height), shadowSoftness, shadowOffset, shadowColor, 0, bounds, base, clip)
// due to shadow draw rectangle with a certain width and height
raw := painter.DrawRectangle(canvas.NewRectangle(fill), width, height, 0, func(in float32) float32 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this - is it the cause of test failures?
If the geometry is not changed then isn't it the same rectangle but drawn at a different offset?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to accomodate shadow the frame must be extended, without this change rectangle always fill in the entire space and the shadow is not visible

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see there is issue on macos platform, could you please show me the rendered images on macos so I can try to investigate the problem because on my local windows platform all tests succeed.

canvas/shadow.go Outdated
// Since: 2.7
type Shadowable interface {
// ShadowPaddings returns the paddings (left, top, right, bottom) of the shadow.
ShadowPaddings() [4]float32
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be derived from the public shadow struct data - what is the purpose of having a new interface with this method when it could be internal to the rendering?

Copy link
Contributor Author

@Vinci10 Vinci10 Jul 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The interface is useful when I have obj fyne.CanvasObject as an argument and want to quickly parse it to the new interface s, ok := obj.(canvas.Shadowable) to get paddings ShadowPaddings() but maybe there is a better approach without using new interface.

// Rectangle describes a colored rectangle primitive in a Fyne canvas
type Rectangle struct {
baseObject
// Support shadow configuration
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Embedding at the beginning of the struct like this will break any code that has done &Rectangle{color.Transparent} - though that is not best practice we try to avoid breaking if we can.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks, I will move the embedding at the end of the struct

@Vinci10
Copy link
Contributor Author

Vinci10 commented Jul 28, 2025

Thanks for working more on this, it is coming together.

I have a few questions at this time:

  1. Isn't the geometry in the software painter wrong? The objects are moving out of their position to accommodate the shadow
  2. The Shadowable interface isn't clear - what is its purpose? It doesn't seem to be needed
  3. The public Shadow type can only be used as embedded in other types - what are your thoughts on this balance?

Thanks for review.

  1. Unfortunately, without changing object position when I expand the frame (to accommodate the shadow) the object is always located on the upper-left corner, in such case I cannot render and test shadow with upper-left offset properly. I am testing changes on WSL2 Ubuntu and the new tests succeed. Not sure what could be wrong :(
  2. With interface it is easy to parse fyne.CanvasObject object to the new Shadowable interface and call ShadowPaddings() method. But probably it is not a good argument. I also thought the new interface would help with adding new shadow types in the future, but I'm not sure if it makes sense.
  3. Sure, sounds good. So should it be a public struct and how should I call ShadowPaddings() method for fyne.CanvasObject objects ? Thanks in advance.

Which implementation is expected ?
a (embedded).

type Rectangle struct {
	baseObject

        // ....

	// Support shadow configuration
	//
	// Since: 2.7
	Shadow
}

b (public field).

type Rectangle struct {
	baseObject

        // ....

	// Support shadow configuration
	//
	// Since: 2.7
	Shadow shadow
}

@Vinci10
Copy link
Contributor Author

Vinci10 commented Jul 30, 2025

I have removed ShadowingRenderer and applied rectangle shadow to the following objects:

Containers:

  • InnerWindow

Widgets:

  • Card
  • Menu
  • Popup

Internal GLFW:

  • MenuBar

I would like to highlight that I set some initial values for the shadow softness and offset to show what it might look like and how the test might change. I was able to update some platform tests to be successful.
Could you please help me with finding proper values for shadow parameters for the objects so I can start fixing tests for mobile and other platforms.

@Vinci10 Vinci10 requested a review from andydotxyz July 30, 2025 15:44
@Vinci10
Copy link
Contributor Author

Vinci10 commented Oct 27, 2025

If you don't mind, I'd like to continue working on this feature for version 2.8.
I removed Shadowable interface, but the tests still need updating.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants