Playnote > Theming > Scripts
Stylesheets can modify the appearance of existing widgets, but they're not able to add new graphical elements, or execute advanced styling logic. Scripts exist to bridge this gap.
Playnote uses AngelScript as its scripting language. If you are interested in writing or tweaking scripts, the first thing to do would be to familiarize yourself with the language documentation. The rest of the page will assume you understand the language, and explain the API contracts that Playnote expects scripts to fulfill.
Value types
The generic and semantic value types made available to scripts are documented in the reference.
Use cases
There are two distinct ways in which scripts are currently utilized:
- Decorators: a script can implement any number of arbitrary decorators by implementing the
IDecoratorinterface any number of times. - Widget scripts: some widgets are script-driven, allowing themes to control all of their drawing logic. To implement one, a specific interface has to be implemented, and it can be implemented only once.
Decorators
Without further ado, the smallest possible decorator that does nothing:
class Noop: IDecorator {
private Drawlist@ shapes;
void init() override {}
void draw(Size) override {}
}
A decorator type is an AngelScript class that implements IDecorator. Each time a decorator is attached to a widget, a new instance of the class is created.
On attachment, the init() method is called. The function must be implemented, but has no required semantics. It is simply the place for code that must run once at creation. The typical use case is creating in advance all the shapes that the decorator plans to draw.
The draw(Size) method is called once per frame. The Size argument is the size of the underlying widget this frame. The origin is always (0, 0); in other words, a decorator works in a local coordinate space and doesn't need to concern itself with the attached widget's position, rotation or scale. Typically, in this method a decorator will position and size the shapes to match the widget they are attached to.
Drawlist@ shapes is a required property. A Drawlist@ is a container of Shape@ instances, which will be drawn to the screen every frame. A Shape@, by itself, will not be drawn even if it exists; it needs to be added to a Drawlist@. Its complete public syntax, including spawn() and detach(), is listed in the reference.
As a reminder, the @ in AngelScript indicates a reference type. Think of a reference like a smart pointer; it is guaranteed valid for as long as you are able to access it. Drawlist@ is an application-managed reference, so it is not possible to make copies of it. Shape@ is script-managed, and you can keep as many copies of it as you want; the underlying shape will only be destroyed after the last reference to it is out of scope.
Note
Internally,
Shape@s are stored as intrusive slotmaps, making them very cheap to create and destroy.
Drawing something
For the decorator to show something on the screen, it must have a shape in its Drawlist@:
class Circle: IDecorator {
private Drawlist@ shapes;
void init() override {
shapes.spawn("circle");
}
void draw(Size) override {}
}
This decorator will draw a @shape circle from the stylesheet, whatever it looks like, at the default (0, 0) offset from widget origin.
For completeness, let's assume the shape is defined as follows:
@shape circle {
circle {
color: rgb(1 1 1);
radius: 8;
}
}
The Shape@ Drawlist@::spawn(string) method does a number of things:
- Creates a
Shape@instance from a@shapeblock with the provided name, - Adds that
Shape@to itself, - Returns a copy of the
Shape@.
The returned Shape@ is a second reference to the same object. This code discards it, but the shape is still being kept alive by the first reference, which is internally held by the Drawlist@.
This is all that's needed for the shape to appear on the screen. Presence in the Drawlist@ is the only requirement for a Shape@ to be drawn; draw() doesn't need to execute any extra code. The purpose of draw() is to make any per-frame modifications to the shape, but we can only do it if we keep around a reference to it:
class Circle2: IDecorator {
private Drawlist@ shapes;
private Shape@ circle;
void init() override {
@circle = shapes.spawn("circle");
}
void draw(Size size) override {
circle.position = size / 2;
}
}
This decorator draws the same circle as before, but always at the exact center of the widget.
The spawned circle is now kept around for later modification - circle and the copy kept by the Drawlist@ refer to the same shape, therefore modifications to circle modify the shape being drawn on the screen. Note that inside init() we assign to @circle, which means rebinding the reference itself, while circle accesses the data inside.
When a shape should stop being drawn after its animation completes, remove it with shapes.detach(circle). It will continue to be on-screen even if no references to it exist, as long as an animation is still ongoing. The complete public Shape@ API is documented in the reference.
Properties
By adding public fields to the decorator class, the decorator can be customized each time it is attached in the stylesheet:
class CircleColor: IDecorator {
private Drawlist@ shapes;
private Shape@ circle;
Color color;
void init() override {
@circle = shapes.spawn("circle");
}
void draw(Size) override {
circle.color = color;
}
}
This decorator can now be used as follows:
text {
@deco circlecolor { /* the class name, lowercased */
color: rgb(0 1 0);
}
}
All text widgets will now have a green circle in the top-left corner.
We could set the color just once in init(), and in this case it would work the same, because color is set to a constant. It is possible though for decorator properties to be animated! In this case, the value of color will autonomously change over time, so we should set it every frame to apply any ongoing animations.
The property can be of any type supported by the stylesheet. The built-in bool and int map to Bool and Integer, respectively. AngelScript enums can be used as well; their allowed values will be lowercased for the stylesheet.
private fields aren't converted into properties, and types with no stylesheet equivalent are ignored. However, it's good practice to mark any fields not meant to be exposed as private.
Hooks
A few properties are defined on Shape@, like position and color that we used earlier. They act on the shape as a whole, affecting every primitive. However, sometimes there's a need to modify a property that's not normally available, or change something about a subset of a shape's primitives. For this, primitive properties can be set to hooks to make them editable from the script.
@shape twothings {
rect {
size: 20 10;
color: rgb(1 0 0);
}
circle {
radius: 8;
color: $color;
}
}
text {
@deco twothings {
color: rgb(0 1 0);
}
}
class TwoThings: IDecorator {
private Drawlist@ shapes;
private Shape@ twothings;
Color color;
void init() override {
@twothings = shapes.spawn("twothings");
}
void draw(Size) override {
twothings.set("color", color);
}
}
In the example above, twothings is a shape with one hook, called $color. This hook sets the color of the circle, without affecting the rectangle. The color is then forwarded from the parameter, so that the user of the decorator gets to control it. If we used twothings.color, the red rectangle would be tinted as well.
Retrieving constants
A stylesheet's constants can be retrieved directly in the script via a global Theme@ object defined in the prelude prepended to every script. Like Drawlist@, it is an application-managed reference. Its complete set of typed accessors is documented in the reference.
@shape circle {
circle {
radius: 5;
color: $color;
}
}
@const palette {
accent: rgb(0.2 0.8 0.3);
}
class CircleConst: IDecorator {
private Drawlist@ shapes;
private Shape@ circle;
void init() override {
@circle = shapes.spawn("circle");
circle.set("color", theme.color("palette.accent"));
}
void draw() override {}
}
The available methods on theme correspond to every stylesheet type, lowercased. Bool is boolean() though, to not collide with a language keyword.
In this case, it's okay to set the value in init(). Only constants that aren't animations can be retrieved this way.
Widget scripts
A widget script follows the same principles as decorators, and can make use of the same features. However, while every decorator implements the same interface, every widget script has its own bespoke interface. For simplicity's sake, we'll explore the example of a simplified Playfield widget script:
Important
Widget script interfaces are not currently documented, since they are going to be changing rapidly while Playnote is in development. Consult the sources and the default theme for reference.
class Playfield: IPlayfield {
private Drawlist@ shapes;
void init(array<LaneType> layout) override { /* ... */ }
void on_note_create(Note &inout note) override { /* ... */ }
void on_note_move(Note &inout note) override { /* ... */ }
}
A widget script can register more than just an interface. Let's catalogue what extra types were made available:
LaneType: in this API, the requiredinit()method is passed an array ofLaneTypes, which is an enum type. This represents the columns of the playfield, such as 7K being a scratch lane and then alternating odd/even lanes.arrayis a built-in AngelScript type.Note: ininit(), the playfield script is supposed to set up the background graphics by adding them toshapes. However, the falling notes are part of a separate drawlist that's not exposed to the script. Instead, the script gets indirect access via an opaqueNotetype.on_note_create()is expected to imbue theNotewith a shape viaNote::set_shape(string), whileon_note_move(), called every frame on every on-screen note, should move it by gaining access to the previously set shape vianote.shape. An&inoutparameter gives indirect access to the object without giving up ownership.
This is just an example of how a widget script might choose to design its API. Generally, the script receives generic events, and can choose to respond to them with arbitrary drawing logic. This way, virtually any appearance can be achieved. A playfield script can implement upscroll, or even a perspective view.