art with code

2008-12-13

Filezoo day 32: drag and drop handling

Added a handler for dealing with data dropped onto the Filezoo window. It copies or moves the source uri list (Konqueror, Nautilus, Firefox and others pass the DnD'd files as \r\n-separated strings of URIs) over to the drag target directory using Gnome-VFS. So now I can drag an image from Firefox onto a Filezoo directory and it'll be copied there.

The implementation was a wee bit challenging, since there was very little documentation for doing DragDrop event handling and perhaps even less for the Gnome.Vfs namespace. Here's what I used:

Handling data dropped on the widget, this is from the widget constructor:

TargetEntry[] target_table = new TargetEntry [] {
new TargetEntry ("text/uri-list", 0, 0),
new TargetEntry ("text/plain", 0, 1),
new TargetEntry ("STRING", 0, 1)
};

DragDataReceived += delegate (object sender, DragDataReceivedArgs e) {
string type = e.SelectionData.Type.Name;
Console.WriteLine(type);
Gdk.DragAction action = e.Context.SuggestedAction;
if (type == "text/uri-list" || (type == "text/plain" && IsURI(e.SelectionData.Text))) {
string data = new System.Text.ASCIIEncoding().GetString(e.SelectionData.Data);
string[] uris = data.Split(new char[] {'\r','\n','\0'},
StringSplitOptions.RemoveEmptyEntries);
string msg = action.ToString() + " " + String.join(", ", uris);
} else {
Console.WriteLine("Got {0}", e.SelectionData.Text);
}
);

Gtk.Drag.DestSet (this, DestDefaults.All, target_table,
Gdk.DragAction.Move
| Gdk.DragAction.Copy
| Gdk.DragAction.Ask
);

Using Gnome-VFS XferUriList to do a copy or move:

public static void XferURIs
(Gnome.Vfs.Uri[] sources, Gnome.Vfs.Uri[] targets, bool removeSources)
{
XferURIs (sources, targets, removeSources, ConsoleXferProgressCallback);
}

public static void XferURIs
(Gnome.Vfs.Uri[] sources, Gnome.Vfs.Uri[] targets, bool removeSources,
Gnome.Vfs.XferProgressCallback callback)
{
Gnome.Vfs.Vfs.Initialize ();
Gnome.Vfs.XferOptions mode = Gnome.Vfs.XferOptions.Recursive;
if (removeSources) mode = mode | Gnome.Vfs.XferOptions.Removesource;
Gnome.Vfs.Xfer.XferUriList (
sources, targets, mode,
Gnome.Vfs.XferErrorMode.Query,
Gnome.Vfs.XferOverwriteMode.Replace,
callback
);
}

public static int ConsoleXferProgressCallback (Gnome.Vfs.XferProgressInfo info)
{
switch (info.Status) {
case Gnome.Vfs.XferProgressStatus.Vfserror:
Console.WriteLine("{0}: {1} in {2} -> {3}",
info.Status, info.VfsStatus, info.SourceName, info.TargetName);
return (int)Gnome.Vfs.XferErrorAction.Abort;
case Gnome.Vfs.XferProgressStatus.Overwrite:
Console.WriteLine("{0}: {1} in {2} -> {3}",
info.Status, info.VfsStatus, info.SourceName, info.TargetName);
return (int)Gnome.Vfs.XferOverwriteAction.Abort;
default:
Console.WriteLine("{0} / {1} {2} -> {3}",
info.BytesCopied, info.BytesTotal, info.SourceName, info.TargetName);
return 1;
}
}

(Now I need a pimpin' progress indicator... maybe an arc to the dir with "incoming $STUFF -." or something with ambient notifications. Maybe just pop up a plain jane Gtk progress dialog :P)

Next features on this path would be dragging things around, navigating while dragging (think Finder's space-to-descend on steroids), and handling cut-copy-paste.

2008-12-12

Simple clock in F# and Cairo


Ported the C# Simpleclock (see previous writeup) over to F# to get to grips with the language and the tools. The resulting Simpleclock.fs is around 10 lines shorter than the C# version and includes a currying-style wrapping over most of basic Cairo.

I like how .NET method arguments are tuples in F#. Having to add type annotations for object parameters is less nifty.

let curry5 f x y z u v = f (x,y,z,u,v)

let arc (cr:Cairo.Context) = curry5 cr.Arc

I don't much appreciate how fscp10.exe takes 5 seconds to start up either.

F# is seriously cool though. I should try porting some parts of Filezoo over and link them in.

2008-12-11

The reason i dig functional languages over C#/Java

Consider this C# snippet to stroke a polygon:

public static void StrokePolygon (Context cr, List<PointD> points)
{
DrawPolygon (cr, points);
cr.Stroke ();
}

public static void DrawPolygon (Context cr, List<PointD> points)
{
cr.NewPath ();
bool first = true;
foreach (PointD p in points) {
if (first) {
first = false;
cr.MoveTo (p.X, p.Y);
} else {
cr.LineTo (p.X, p.Y);
}
}
}

And its Haskell equivalent:

strokePolygon = strokeWith drawPolygon
drawPolygon p = do { newPath; drawSubPolygon p }
drawSubPolygon (x:xs) = do { moveToP x; addToPolygon xs }
addToPolygon = mapM_ lineToP
strokeWith = doWith stroke
doWith g f x = do { f x; g }
moveToP = uncurry moveTo
lineToP = uncurry lineTo

The C# version is 379 characters long. The Haskell version is 283 characters long. The C# version defines two reusable functions. The Haskell version defines eight.

In the same amount of characters it takes to write a single function in C#, you can (and usually will) write a whole library in Haskell.

To really make the comparison, consider adding functions to draw filled polygons and filled and stroked circles.

public static void FillPolygon (Context cr, List<PointD> points)
{
DrawPolygon (cr, points);
cr.Fill ();
}
public static void FillCircle (Context cr, PointD c, double r)
{
DrawCircle (c, r);
cr.Fill ();
}
public static void StrokeCircle (Context cr, PointD c, double r)
{
DrawCircle (c, r);
cr.Stroke ();
}
public static void DrawCircle (Context cr, PointD c, double r)
{
cr.NewPath ();
cr.Arc (c.X, c.Y, r, 0, Math.PI*2);
}

And the Haskell version:

fillPolygon = fillWith drawPolygon
fillCircle c = fillWith (drawCircle c)
strokeCircle c = strokeWith (drawCircle c)
drawCircle c r = do { newPath; addCircle c r }
addCircle (x,y) r = arc x y r 0 (pi*2)
fillWith = doWith fill

444 characters in C#, 226 in Haskell. Six functions in Haskell, four in C#. The thing holding back C#'s character count is the heavy function signature boilerplate. In each of the four C# functions above, the function signature has more characters than the function body.

Filezoo day ...I don't know, let's say 31.

Filezoo. A few things.

Had this random hang at application startup. I don't know what was causing it, but disabling the AOT compilation pass made it disappear. So there.

Config now has hooks for using the Gtk theme colors for the application background et al. But I don't have a way of drawing pretty directories for light backgrounds so I disabled it. And black on white directory labels really make URW Gothic L cry.

Moved the panel controls to a whole new widget. Added the widget to the stand-alone windows. Hurrah for DWIM bar!

Some UTF-8 filenames can crash the renderer. I don't know wtf is up with that. On reaching a certain size, the font just completely blows up and the render target _vanishes_.

I didn't really have any particular goal for this day, maybe it's time to start doing the one-month roundup and really start focusing on the UI side of things for the next month.

On missing features. There's no scrollbar, there's no way to zoom to readable size from the keyboard, there's no way to control view settings from the DWIM bar. Directory view settings aren't remembered. The zoom out navigation is jumpy and you can't pan between dirs that are siblings to the current dir. It would help if text files were rendered inside their boxes. Having some sort of extended preview / reading functionality would be cool, it's often handiest to do read-in-place and edit-in-place instead of "pop up an application that hopefully opens your file and wait for ten seconds while it loads." Eaglemode does it the right way, yes.

Making a nicer theme system might be useful. And configuration and menu scripting. Saving config to GConf and durr. It's kind of hard to find the willpower to work on that stuff.

The core rewrite. Hmm. I would like it to be done. And I really wouldn't like to do it.

Something I think I will do (totally lying here) is port the renderer over to OpenGL. And bring back the lens flare. A shining 100FPS or bust!

2008-12-10

Drawing trees with Haskell and Cairo


I'd like to draw a tree, but how? Let's start by writing a simple draw loop:

module Main where
import Graphics.Rendering.Cairo
import Canvas

main = canvas draw 600 600

draw w h t = do
color white
rectangle 0 0 w h
fill
color black
drawTree w h t

Then, what is a tree? A tree is sort of a recursive forking function. A scanl towards the sun. Every year its branches grow in thickness, possibly forking.

Let's define a simple branch as a function of age and angle. A branch of age 0 has no forks. A branch of age N has 2 sub-branches of age N-1.

branch 0 angle = [map (rotateP angle) [(0,0), (0, -1)]]
branch n angle =
this ++ subBranches
where
this = branch 0 angle
[[_,(x,y)]] = this
subBranches = map (map (translateP x y)) (left ++ right)
left = branch (n-1) (angle-pi/8)
right = branch (n-1) (angle+pi/8)

To draw the branches, we need to write the drawTree procedure. Here's one that draws a tree of age 7 and rotates it in the middle of the screen:

drawTree w h t = do
translate (w/2) (h/2)
rotate t
mapM_ strokeLine tree
where tree = map (map (uscaleP 25)) $ branch 7 0

You can see the result on the right. Not the prettiest tree in the land. Let's make the branches get thicker with age.

To draw lines of different thickness, we need to add the thickness information to the line data structure. Previously it was a list of (x,y)-tuples, with width it becomes a (lineWidth, (x,y) list)-tuple. A couple combinators will help here:

strokeWidthLine = tupleDo lineWidth strokeLine
mapWidthLine f = fupleR (map f)

fupleR f (a,b) = (a, f b)

Then rewrite branch and drawTree to use width-carrying lines:

drawTree w h t = do
translate (w/2) h
mapM_ strokeWidthLine tree
where tree = map (mapWidthLine (uscaleP 25)) $ branch 8 0

branch 0 angle = []
branch n angle =
(thickness, points) : subBranches
where
points = map (rotateP angle) [(0,0), (0, -1)]
thickness = n
[_,(x,y)] = points
subBranches = map (mapWidthLine (translateP x y)) (left ++ right)
left = branch (n-1) (angle-pi/8)
right = branch (n-1) (angle+pi/8)


Now the tree grows from the bottom of the screen and looks a bit more aesthetically pleasing. Next we could make the branches rotate and grow with an upwards bias. Compute distance from up-vector and scale points and the angle accordingly. Something like this:

da = angularDistance 0 angle
scale = 3 * ((1-(abs da / pi)) ** 2)
points = map (rotateP (angle + da/3) . uscaleP scale) [(0,0), (0, -1)]

And then, hmm, random angles for the branches? That needs a bit of extra work. The random number generator is in the IO monad, whereas draw is in the Render monad, and branch is a pure function. So, extend main to get a pure list of random Doubles, then pass that to draw, which passes it to drawTree and branch.

main = do
gen <- getStdGen
let ns = randoms gen :: [Double]

canvas (draw ns) 600 600

draw ns w h t = do
color white
rectangle 0 0 w h
fill
color black
drawTree ns w h t

drawTree ns w h t = do
translate (w/2) (h+5)
mapM_ strokeWidthLine tree
where tree = map (mapWidthLine (uscaleP 25)) $ branch ns 8 (pi/2*sin t)

And make branch do something with it:

branch _ 0 _ = []
branch (r1:r2:rs) n angle =
[...snip...]
left = branch (takeOdd rs) (n-1) (angle - r1*pi/4)
right = branch (takeEven rs) (n-1) (angle + r2*pi/4)

takeOdd [] = []
takeOdd [x] = []
takeOdd (_:x:xs) = x : (takeOdd xs)

takeEven [] = []
takeEven [x] = [x]
takeEven (x:_:xs) = x : (takeEven xs)

The result of all this tomfoolery is a tree that looks a bit more natural than the geometric trees above.

The trees at the top of this post use random numbers for scaling the branches as well, so they're even more noisy.

Here's the source code: tree.hs and canvas.hs.
Compile by doing ghc --make tree.hs canvas.hs -o tree

Filezoo, day 30-ish: painted myself in a corner

Ok, committed a few bugfixes to the traversal code, and now the size totals work pretty nicely, though likely allocate way too much. A bigger problem is that the current FSCache design is suffering from a couple decisions made at last rewrite time. In particular, the drawing model uses the FSCache directly, which means that it's race-condition-prone. If a filesystem change happens during drawing a frame, what gets drawn beyond that point is anyone's guess. It'll be fixed by the next frame though.

The New And Improved Design Suffering From Version 2 Syndrome I have is to split the FSCache into three caches instead of the current two: Entry, Thumbnail -> Entry, StatInfo, Thumbnail. The idea being that Entry would be a very light-weight thing used by the recursive traversal, and StatInfo contains all the stat struct information. StatInfo cache and Thumbnail cache would have LRU expiration to limit memory use. The Entry cache wouldn't really need expiration, as it's not going to be much larger than 20 megs (but it is a possible memory leak...)

Then the drawing model builder wouldn't use the FSCache directly, but build a temporary drawing tree that's always ready to draw. Currently you can get drawing flicker on filesystem changes and traversal changes because the changes set ReadyToDraw = false for that part of the tree, and the model builder needs to revisit and redo the layout to make it drawable again.

Building a drawing tree separate from the FSCache would allow propagating the FSCache changes to the drawing model in a controlled manner, so that the tree is always in a drawable state and there's no flicker and you can't have race conditions involving filesystem changes and the drawing thread.

Why the new design suffers from V2S is that it requires a well-nigh complete rewrite of the cache behaviour and the draw model building behaviour. Yesterday I wrote a few hundred lines of code in an effort to do the split, but as I didn't do it by small compilable changes, it very quickly veered into the "I'm never going to get this to work"-state of lost confidence. Lesson learned once again.

So. Stashed the changes in a branch, forked a new branch from master and backported some of the small changes back. I also fixed some buggy behaviour, now filesystem changes aren't as likely to hose the drawn state and UTF-8 filenames are handled correctly by traversal (yay for new System.Text.UTF8Encoding().GetString(byte[]).) I think I'll spend the rest of the day doing something with a high return on investment, a.k.a. UI work.

Then piecemeal porting of FSCache over to a cleaner system? Don't break the build!

2008-12-08

Distorted hex


Perspective projection gone horribly wrong. The gist of it is

cylinderProjection r (x, y) =
(mx * 200/z, my * 200/z)
where mx = r * sin (x/r)
my = y
z = 300 + r + r * cos (x/r)

Here the source: distorted_hex.hs

Blog Archive