Implementing Mouse Lock, part II
_This post is part of a series I’m writing about my work to implement the Mouse Lock API in Mozilla. I'm doing the work with students in my Mozilla Open Source course at Seneca College, and so theses posts are intentionally didactic and self reflective. The aim of the series is to show how a new feature gets implemented in Firefox. Please note that all code discussed is unreviewed and not part of a shipping Firefox at this point.
I wrote previously about my intention to implement the Mouse Lock API, and to do it with my students. Since then I've been traveling and speaking at conferences, but during that time both my students and myself have been busy. It's a good thing because everywhere I went I got asked the same question, "When will mouse lock be done???? Can I try it yet???" So let's dive in and write some code.
As we began our work, I was fortunate enough to have Mike Shaver, who formerly lead Mozilla's engineering and is now helping to do the same with Facebook's, come and speak with the students. I asked him how he would approach a technical problem like this, what strategies to employ, what pitfalls to avoid. He had some invaluable advice, including:
- Start by building Firefox, make a change (do a printf), blog it. Don't start with the Mouse Lock API.
- Write prose about what you know. "Produce an understanding"
- Ambient learning: not everything about what you're looking at, just enough to get things done."
- "Finding is making progress"
- Don't solve big problems--lots of small problems
- Work together with people, don't do it alone.
- You will find bugs in the code, spec, and docs. Sometimes when things don't make sense it's because they don't make sense--it's not just you.
- Don't write too much code without tests
First, build Firefox, make a small code change, write about it, produce an understanding. Firefox has become easier to build over the years, but it's still complicated, especially for the first time developer who hasn't worked on code this large before. I let the students try it on their own before taking a lecture to explain how Mozilla's build system works. They wrote about their experiences, created an FAQ of common issues they hit, and even found a bug in our build system ("You will find bugs...it's not just you."). Next they tried changing something in the existing source, adding a printf, and figured out how to get it to show up when they ran the build.
After they'd learned how to build and make simple code changes, I asked them to see if they could figure out where the mouse event attributes (clientX, screenX, etc.) were being set. "See if you can break things." I showed them how to find their way around the source using MXR and DXR, (e.g., finding mouseevent) but gave them no understanding of the code. I wanted them to grope in the dark and learn to make guesses, try things, see what happens, repeat. I also wanted them to learn just enough in order to have something happen, without worrying about trying to understand everything. This too went well, which is a relief, since none of the students have worked on Firefox before (protip for other profs: your students can do work like this if you let them and encourage them).
Following Mike's advice that we not solve big problems, and instead work on small, actionable tasks, the logical thing to do is to implement a shell without worrying about doing the actual mouse locking. That is, add navigator.pointer with the islocked() and unlock() methods, leaving the guts of lock() until later. To do so we have to understand how Mozilla's interfaces and implementations work--something I've written about here.
The Mouse Lock spec defines the Web IDL for us, which we can modify slightly in order to add nsIDOMMouseLockable.idl, and to change nsIDOMNavigator.idl in order to add the pointer attribute. Having created/updated these interfaces, we now need to work on the implementation. nsIDOMMouseLockable.idl needs nsDOMMouseLockable.h and nsDOMMouseLockable.cpp (notice the I is missing in the name, and how isLocked/unlock become IsLocked/Unlock in C++. Notice too how nsIDOMMouseLockable.h is included, but appears nowhere in the source--it will get generated at build-time by the xpidl compiler from nsIDOMMouseLockable.idl). We also need to update the implementation for nsIDOMNavigator.idl, which lives in Navigator.h and Navigator.cpp so that we hand out an instance of a MouseLockable when pointer is accessed (notice how pointer becomes GetPointer in C++).
This code also requires a bunch of build system changes, in order for our new files to get picked-up and built by the xpidl or C++ compilers. We also need to let the DOM know about our new type. When doing work like this, a large code base is our friend, because it provides ready examples of how to do most of the things you want to do. However, I still struggled to get it right, and had to ask for help ("Work together with people, don't do it alone").
var pointer = navigator.pointer; console.log("pointer.islocked() == " + pointer.islocked()); pointer.unlock();
On the one hand, not very useful. On the other, and as one of my students said today, "WOW!" We've successfully added the beginnings of our DOM implementation. It's a good time to consider one of the final things Mike said when he visited, namely, "Don't write too much code without tests." We actually have enough code now to write a useful set of tests using Mozilla's Mochitest suite. Our first tests will simply check that navigator.pointer exists, that it returns the right type of object, and that it has the appropriate methods. We'll even write a test for lock() and mark it as todo so we won't forget to turn it on later.
This is a good time to pause. Part of "work on many small problems" is also allowing yourself to enjoy achieving things along the way to your larger goal. We're still many miles from a complete Mouse Lock implementation, but we're not at zero anymore. We're probably going to have to change things we've written, rename files, move things around, etc. but it doesn't matter at this stage, and shouldn't stop our progress as we experiment and learn. Right now we're framing the house, and fine details like that will come later, especially during review.
I've asked the students to add movementX and movementY to the MouseEvent object for next class, and then we can start dipping down into the platform bits, where we'll actually do the locking. If you're playing along at home, feel free to try it yourself and leave a comment about your success or troubles. If you have ideas, comments, or corrections, throw those in the comments, too.