Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FlatSVGIcon color filters #303

Merged
merged 4 commits into from
Apr 18, 2021

Conversation

xDUDSSx
Copy link
Contributor

@xDUDSSx xDUDSSx commented Apr 8, 2021

This pull request adds the ability to specify an RGBImageFilter to a FlatSVGIcon.

I was looking for this functionality when developing a custom swing component to use with FlatLaf. This component needed to display SVG icon silhouettes that would have a different color for a hover effect.
Specifying a filter allows me to change every SVG icon to a single color silhouette as well as change its color on the fly.

That could now be accomplished with

icon.setFilter(new FlatRGBFilter() {
	@Override
	public Color filterRGB( Color c ) {
		return c.brighter();
	}
});

That example uses a new class that subclasses the standard RGBImageFilter to pass and return a Color object for simplicity.
An RGBImageFilter can be passed too, as well as the com.formdev.flatlaf.util.GrayFilter like so

icon.setFilter(new GrayFilter(50, 0, 100));

I'm sure some sort of a basic ability to change SVG icon colors would be useful. I saw that the ability to do so is already in the FlatSVGIcon, just not exposed publicly.

Here's an example of the component I was working on, it's a sidebar that can easily draw the hover effects using FlatSVGIcon.
sidebar

@DevCharly
Copy link
Collaborator

Interesting idea 👍

There is already a way to map own colors, but this is global for all icons and not dynamic. E.g.:

FlatSVGIcon.ColorFilter globalColorFilter = FlatSVGIcon.ColorFilter.getInstance();
// map red to orange
globalColorFilter.add( Color.red, Color.orange );

I'm not sure whether RGBImageFilter is optimal here.
It is designed for bitmap images and looks like overkill for the use in FlatSVGIcon.
I know, RGBImageFilter is already used in FlatSVGIcon for disabled icons,
but the only reason for this is that the gray filter comes from the Laf
and must work for bitmap images.

Probably a simple java.util.function.Function<Color, Color> would do:

public void setFilter( Function<Color, Color> filter ) {
...
}

Then the example from the first post becomes:

icon.setFilter( color -> color.brighter() );

What do you think?

BTW please rebase the PR branch to main. Currently it is based on a 3 weeks old commit.

@xDUDSSx
Copy link
Contributor Author

xDUDSSx commented Apr 8, 2021

I agree that there is no need to use the RGBImageFilter for this and the java.util function would work just fine, the only drawback I see is that the already existing GrayFilter can't be used with it. But I'm sure something similar can be written that would allow easy brightness/contrast modifications. So yeah, what you're suggesting would certainly work nicely.

However, I see you mentioned the global color map that can be set using the ColorFilter. That got me thinking that a good way to handle could be to expand the ColorFilter class itself to act not only as a color mapper but as well as a way to directly modify the color. It could then be set as a filter for a single FlatSVGIcon instance as well as a global modification that goes beyond the scope of just mapping colors. From a user perspective, it would be easier to modify rgb values than to set a color map. It would also allow dynamic color changes like with the single FlatSVGIcon instance.

2 hours later

I got a little carried away.
So I tried a few ways to do what I said. Initially, I thought about making ColorFilter abstract and make its current implementation a subclass. But that would ruin already existing code and not really make things easier.

So instead I made ColorFilter fill both roles. I added a new field to it private Function<Color, Color> colorFilter = null; and some new constructors to get more control. It still works exactly the same as before. But now additionally you can create it using new ColorFilter( color -> color.brighter() ) (notice how you can still use a lambda). Such a ColorFilter can then be passed to a FlatSVGIcon instance using icon.setFilter() and it works exactly like before. It plays nice with the mappings too and this color modification gets applied to already mapped colors.

So besides setting a filter to an instance, you can create a global icon filter using for example

FlatSVGIcon.ColorFilter.getInstance().setFilter( color -> Color.RED );

and as I've said already it will work with mappings at the same time

FlatSVGIcon.ColorFilter globalColorFilter = FlatSVGIcon.ColorFilter.getInstance();
globalColorFilter.add( Color.red, Color.orange );
globalColorFilter.setFilter( color -> color.brighter() );

And just to recap here is a compact way of setting an instance filter

icon.setFilter( new ColorFilter( color -> color.brighter() ) );

I added some basic Javadoc wherever I could to clarify things. Also to test it I added a little demo to the flatlaf-demo extras panel. Here's a picture. Just for fun, I created a rainbow icon to show off how it can work dynamically.

svgicondemo

Also, I rebased the branch to the latest commit. I hope that's what you meant :D.

…the ColorFilter class and can work globally too. Added related demo components to flatlaf-demo extras tab.
… a little and added a utility method to ColorFilter to easily create a brightness/contrast/alpha filter.
@xDUDSSx
Copy link
Contributor Author

xDUDSSx commented Apr 9, 2021

Also, I think it would be interesting to expand this functionality to raster icons as well. There could be a universal FlatIcon class that would have these filtering capabilities using the quite intriguing GraphicsProxy. It could delegate .svg icons to FlatSVGIcon and other image types to regular ImageIcon.

For applications that are using raster icons or are on their way to convert to .svg icons, it would unify Icon declaration to just using a catchall FlatIcon (or similarly named) instead of using ImageIcon for some and FlagSVGIcon for others.

Now I'm guessing that for raster images, the current color mapping defined by the themes would not work too well**. But it would be interesting to maybe explore some option to use these filters to automatically adjust icon color (be it raster or svg) based on their contrast with the background or something similar. It could also go as far as generating dark variants for icons that don't have one explicitly provided.

Just an idea 😅

** Because pixels can have slightly different colors that look the same from afar.

@DevCharly
Copy link
Collaborator

Fantastic idea to use class ColorFilter also for single icon instances 👍

I'll have a closer look soon...

@xDUDSSx
Copy link
Contributor Author

xDUDSSx commented Apr 15, 2021

I kinda already mentioned the possibility to create dark theme icons automatically or similar.
I was just trying to use some icons from UIcons that come in the form of just simple silhouettes. Many packs use this style and unlike colorful icons, these icons are usually just black or some lighter color in dark themes. A black icon like this is then unusable in a dark theme and to fix that you'd need to, in case of UIcons recolor it manually and rename it.

This could be easily simplified by obviously setting a ColorFilter like previously discussed, but I think I would be nice to add a FlatSVGIcon constructor that would allow you to set a color for both Light or Dark themes. As a constructor it would function for per icon basis, so unlike using a global mapping, for example, icons that don't use this silhouette style won't be affected by it. You could then set up a little loader method just for icons using a certain style.

The only requirement is the FlatSVGIcon object to be able to figure out which theme is currently in use, I think there is a FlatLaf way to figure that out I just can't recall it.

The constructor could look something like new FlatSVGIcon("path/to/icon", Color.LIGHT_GREY, Color.BLACK). Specifying null for either could be signaling to use the original color. Then in the impl an appropriate ColorFilter would be created.

TLDR: Recoloring icons manually is tedious, same with renaming them and the simple fact is most icon repositories won't give you a _dark variant by default.

Also, I'm glad you liked the ColorFilter idea 😄

DevCharly added a commit that referenced this pull request Apr 18, 2021
@DevCharly DevCharly merged commit e9b2f17 into JFormDesigner:main Apr 18, 2021
@DevCharly
Copy link
Collaborator

@xDUDSSx thanks for your ideas and your contribution. I've merged it with following changes:

  • renamed FlatSVGIcon.setFilter(...) to setColorFilter()
  • renamed ColorFilter.setFilter(Function) to setMapper(Function)
  • replaced ColorFilter.createGrayFilterFunction(int,int,int) with universal createRGBImageFilterFunction(RGBImageFilter)
  • ColorFilter: use default color palette mapping only in global filter

Additionally, I've made following changes/improvements:

  • if the per-icon color filter modifies the color, then the global color filter is not used
  • different mappings for light and dark themes in single color filter
  • fluent API
  • reduce memory usage by creating hash tables only if needed

Color filter now supports different mappings for light and dark themes. Following example maps gray (or #808080) used in SVG, either to lightGray (in light themes) or to darkGray (in dark themes):

colorFilter.add( Color.gray, Color.lightGray, Color.darkGray );

Class FlatSVGIcon.ColorFilter now has a fluent API, which makes code easier/shorter:

icon.setColorFilter( new ColorFilter().add( Color.black, Color.darkGray ) );

or

icon.setColorFilter( new ColorFilter()
    .add( Color.black, Color.darkGray )
    .add( Color.red, Color.magenta )
    .add( Color.gray, Color.lightGray, Color.darkGray ) );

Made also some changes to the Demo:

  • animate "rainbow" icon only if tab is shown
  • re-added "Toggle red"

image

@DevCharly DevCharly added this to the 1.2 milestone Apr 18, 2021
@DevCharly
Copy link
Collaborator

@xDUDSSx regarding the idea for an additional constructor: well, I'm not a fan of this idea for various reasons:

  • There are already a lot of constructors in FlatSVGIcon and another one would make it even more complicated because this would probably need some variants (with classloader, with scaling, ...)
  • It is possible to use the setter after creating the icon.
  • It is also possible to use a helper method if necessary.
  • When using various icon sets, then it is probably best to create different color filters for each icon set and reuse them for multiple icons.

@xDUDSSx
Copy link
Contributor Author

xDUDSSx commented Apr 24, 2021

@DevCharly
Quite a fan of the changes you've made, sorry for not responding earlier.
Not creating additional constructors is reasonable, it was more of an example.

I like the way color mappings and the color function work now, I'm just missing a general way of specifying dark/light theme colors without worrying about the actual color of the icon.

Here's what's possible right now:

icon.setColorFilter(new ColorFilter((color) -> {
	if (FlatLaf.isLafDark()) {
		return Color.red;
	} else {
		return Color.green;
	}
}));

This works, but it feels like there could be a better way.

icon.setColorFilter(new ColorFilter(color -> UIManager.getColor("Label.foreground").brighter()));

This also works quite nicely, might even give interesting results for different themes.

Anyway, I just want to leave these examples here as I find them quite useful. I don't know if it's necessary to make any additional changes to make this sort of a thing more accessible. It might just be enough to mention these use cases of the ColorFilter somewhere, possibly just in the javadoc.

Glad that I could help 😃

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.

2 participants