What's This All About?
Malison is a simple, easy-to-use library for doing console-style interfaces in C#. By "console-style", I mean it lets you build apps that look like this:
I developed it for use on a roguelike game I'm working on called Amaranth. My goals with Malison are:
- Be easy-to-use. Even a user interface as simple as a console one still tends to have a lot of code for drawing stuff. I want that code to be as short and clear as possible. To that end, using Malison looks a bit like writing code in a DSL for terminal drawing. You'll see what I mean in a bit.
- Support different renderers. In the future, I may want to port Amaranth to XNA, WPF or Silverlight. As long as I stick with the console paradigm, Malison should be the only thing that needs to care about that. Code that writes to the console (i.e. the game engine) shouldn't be coupled to how it's rendered.
How Do I Try It?
Simple! Just pull down the code, build it, and run the included example app. I'm using Visual Studio Express 2008 ('cause it's free). Malison requires .NET 3.5. The example app doesn't do much, but it shows you what it looks like, and it has a bit of code so you can see what it looks like to use the API.
I'm Too Lazy to Run an App. Show Me Some Code Here!
Oh, fine then. To draw something, you need a terminal: an object that implements
ITerminal. There are a couple of ways to get one, but the easiest is just to create one:
ITerminal terminal = new Terminal(80, 25);
Now you've got a blank 80 x 25 character terminal you can start drawing on. Writing some text is simple:
That will draw the text in the top-left corner in white on black, the default colors. So what if you want a different position or color? This is where Malison gets interesting. Position, bounding box, foreground color, and background color are specified using indexers. Like this:
terminal[2, 5][TermColors.Green].Write("Hello again!");
That will write another line of text two rows down and five columns to the right from the top-left corner, and in bright green. These indexers can be freely combined and support a couple of different overloads. Here's some more examples:
terminal[TermColors.Red, TermColors.DarkBlue].Write("Red on blue, at the top-left corner."); terminal[-1, -1].Write("Negative coordinates are from the bottom-right corner.");
There are a few other things you can draw aside from text:
terminal.Clear(); // clears the entire terminal terminal[2, 3, 4, 5].Clear(); // clears just a 4x5 rectangle two columns and three rows from the corner terminal[0, -4].Fill(TermColors.Green); // fills the bottom four rows with green terminal[2, 3, 4, 5].DrawBox(); // draws a box outline terminal[2, 8, 4, 5].DrawBox(DrawBoxOptions.DoubleLines); // draws a box double-outline
What? How Do the Indexers Work? I Don't Get It.
I'll admit it's a little strange. Here's the underlying Philosophy of Malison (tm). A terminal has two kinds of state associated with it: character data and the cursor. Character data is the state you think about: it's the grid of letters and their colors. When you write to a terminal, you're changing character data. The cursor is the ephemeral state a terminal uses to decorate what you mean when you tell it to draw something. When you say
Write("Hi!"), the terminal's cursor dictates where and what color to use to write that.
Seems pretty simple. Here's where it gets kooky. A terminal's cursor never changes. While the character data for a terminal is mutable (i.e. it can change), cursor data is immutable. There is no
MoveCursor() method in Malison.
Instead there are the indexers. Every time you call an indexer, like
terminal[TermColors.Blue] you're creating a new terminal that shares character data with the old one, but has its own cursor. When you then write to that new terminal, you write to the same character data, but with a different cursor (blue, in this case). This lets you do neat stuff like this:
// setting the color each time is lame, let's reuse it ITerminal tinted = terminal[TermColors.Black, TermColors.Orange]; tinted.Write("This will be orange."); tinted[0, 1].Write("This too!"); // and we can also make windows ITerminal window = terminal[4, 7, 20, 10][TermColors.Blue]; window[0, 0].Write("I'm a window!"); window[0, 1].Write("But I look just like my own terminal.");
The second little example there is the real reason Malison works this way. Most UI systems are built out of a tree of controls. Each control has a rectangular boundary that it draws in, and some number of child controls that fit inside that boundary. To draw the UI, you traverse the tree, calculate the local bounding box for each control and let it draw itself into that window. Malison supports that windowing concept implicitly.
By making the terminal cursors immutable, we also avoid a common class of bugs. If rendering code can change the terminal state, then different pieces of rendering code can accidentally interfere with each other. Lets say we have two pieces of text to draw. The first one wants to be blue, so its sets the cursor to blue and then draws. Unfortunately, it forgets to set it back. The second piece of text wants to use the default color, but it incorrectly draws in blue. Oops.
Immutable cursors fix that. When the first control "sets" the terminal to blue, it's actually creating a new terminal that the second control will never even see. Problem solved.
Isn't That Really Slow?
It isn't, actually. The cursor data is just a bounding box and two colors. The actual character data is shared, so creating a new terminal based off an existing one takes less time than drawing a single character on screen. It's speedy.
OK, I'm Sold. Now What?
Start playing around with it! If you've wanted to make a roguelike or some other app that could use this kind of UI but didn't have a nice curses library in C# to build on, now you do. If you want to see a good-sized chunk of code that uses Malison, Amaranth has a bunch, particularly the UI and TermApp projects.
If you run into questions, bugs, issues, etc., let me know!.