I'm developing a TUI library in C# and I need advice on how to do color themes for display objects. Objects that can be drawn on the screen all inherit from this interface:
public interface IDrawable
{
Area ScreenArea { get; }
List<char[]> DisplayChars { get; }
//some other properties...
}
Or rather, more specifically, the interfaces for each drawable object implements this interface (IWindow
is a IDrawable
). Each IDrawable
is drawn on a specified part of the console window represented by the Area struct:
public struct Area
{
public readonly int EndX;
public readonly int EndY;
public readonly int Height;
public readonly int StartX;
public readonly int StartY;
public readonly int Width;
public Area(int startX, int endX, int startY, int endY)
{
StartX = startX;
EndX = endX;
StartY = startY;
EndY = endY;
Height = endY - startY;
Width = endX - startX;
}
/// <summary>
/// Get the overlapping area between this area and another.
/// </summary>
/// <param name="refArea"></param>
/// <returns>Overlap area relative to the upper left corner of the ref area.</returns>
public Area OverlapWith(Area refArea)
{
//....
}
}
The actual drawing of objects is handled by methods in a static Display
class, which call Console.Write()
on each element in DisplayChars. I would like for each class that inherits from IDrawable
to be forced to implement its own rules for how its area can be divided into separate areas of color, for example, popup windows might have separate colorable areas for its outer borders, its title (within its outer border), and its inner area.
I've been tossing over how to do this in my head for a while now. I need to make a type, ColorScheme
, to contain the rules for what characters to write in what color. I decided the best way to do this would be to make it an abstract class, which contains a list of "sub-areas" that colors can be applied to separately.
I'd like for each non-abstract IDrawable
to have to implement its own class inheriting from ColorScheme
. For instance, the abstract Window : IWindow
class would have no such implementation, but PopupWindow : Window
class would have to have a corresponding type of PopupWindowColorScheme : ColorScheme
in which the author of PopupWindow
would define how to split the class' Area
into separate regions. Each PopupWindow
would have its own instance of this type to contain its specific colors.
Is this possible? If not, is there another way to force authors of IDrawable
types to specify a method for splitting up their areas into colorable regions?
You can't force each IDrawable
to have a unique implementation of ColorScheme
(e.g. multiple different implementations of IDrawable
could use PopupWindowColorScheme
). However, you can use generic type constraints to add additional requirements for implementing your interface, like this:
public interface IDrawable<TColorScheme>
where TColorScheme : ColorScheme
{
Area ScreenArea { get; }
List<char[]> DisplayChars { get; }
//some other properties...
TColorScheme ColorScheme { get; }
}
Now, every implementation of IDrawable
needs to specify a type of ColorScheme
to use. But a consumer could just implement IDrawable<ColorScheme>
which sort of defeats the purpose (depending on your requirements). We can go a little further:
public interface IDrawable<TColorScheme>
where TColorScheme : ColorScheme, new()
{
}
public abstract class ColorScheme { }
Here, since ColorScheme
is abstract, and the generic type constraint requires the provided type parameter to implement a parameterless constructor (new()
), ColorScheme
itself cannot be used as a parameter. Any implementing class would need to specify a custom implementation of ColorScheme
that provides a public, parameterless constructor.
But we can go even further:
public interface IDrawable { }
public interface IDrawable<TDrawable, TColorScheme> : IDrawable
where TDrawable : IDrawable, new()
where TColorScheme : ColorScheme<TDrawable>, new()
{
object ScreenArea { get; }
List<char[]> DisplayChars { get; }
//some other properties...
TColorScheme ColorScheme { get; }
}
public abstract class ColorScheme<TDrawable>
where TDrawable : IDrawable, new()
{
}
Here, each implementation of IDrawable
has to specify what ColorScheme
it uses and each ColorScheme
also has to specify what IDrawable
it applies to. And because each requires a parameterless constructor, neither would be able to specify the common base type. Implementing this now looks a bit strange:
public class MyDrawable : IDrawable<MyDrawable, MyColorScheme> { }
public class MyColorScheme : ColorScheme<MyDrawable> { }
It's still possible to implement a reusable ColorScheme
or IDrawable
, (e.g. MyOtherDrawable : MyDrawable
uses MyColorScheme
). However, in my opinion, this is starting to get rather cumbersome and tedious to implement. In general, unless you have technical reasons why you have use a type constraint, I'd avoid using it, as you'll often find it too limiting in the future.