This directory contains an example application which draws an animated bouncing ball using Scenic. This README explains how it's built.
From garnet, this can be run with:
present_view fuchsia-pkg://fuchsia.com/bouncing_ball#meta/bouncing_ball.cmx
From topaz, this can be run with:
sessionctl add_mod fuchsia-pkg://fuchsia.com/bouncing_ball#meta/bouncing_ball.cmx
In garnet, Alt
+Esc
toggles back and forth between the console and graphics.
For an application to draw any UI, it first needs to implement a service to expose that UI externally. That protocol is called ViewProvider
.
First, let‘s publish our ViewProvider
protocol to our outgoing services. We don’t do anything with incoming request for a ViewProvider
yet.
int main(int argc, const char** argv) { async::Loop loop(&kAsyncLoopConfigAttachToThread); std::unique_ptr<component::StartupContext> startup_context = component::StartupContext::CreateFromStartupInfo(); // Add our ViewProvider service to the outgoing services. startup_context->outgoing().AddPublicService<fuchsia::ui::app::ViewProvider>( [](fidl::InterfaceRequest<fuchsia::ui::app::ViewProvider> request) { // TODO: Next step is to handle this request. }); loop.Run(); return 0; }
Next, let‘s implement ViewProvider
. ViewProvider
implements one method, CreateView
. CreateView
is called by clients of our ViewProvider
when they want our process to create UI. Most often, this is when a parent process (such as a Session Shell) wants to show a child process’s UI.
This implementation of ViewProvider
doesn't do anything in CreateView
quite yet.
class ViewProviderService : public fuchsia::ui::app::ViewProvider { public: ViewProviderService(component::StartupContext* startup_context) : startup_context_(startup_context) {} // |fuchsia::ui::app::ViewProvider| void CreateView( zx::eventpair view_token, fidl::InterfaceRequest<fuchsia::sys::ServiceProvider> incoming_services, fidl::InterfaceHandle<fuchsia::sys::ServiceProvider> outgoing_services) override { // TODO: Use the |view_token| and Scenic to attach our UI. } void HandleViewProviderRequest( fidl::InterfaceRequest<fuchsia::ui::app::ViewProvider> request) { bindings_.AddBinding(this, std::move(request)); } private: component::StartupContext* startup_context_ = nullptr; std::vector<std::unique_ptr<BouncingBallView>> views_; fidl::BindingSet<ViewProvider> bindings_; };
Next, let's wire up this class in main()
:
int main(int argc, const char** argv) { async::Loop loop(&kAsyncLoopConfigAttachToThread); std::unique_ptr<component::StartupContext> startup_context = component::StartupContext::CreateFromStartupInfo(); // ** NEW CODE **: Instantiate our ViewProvider service. ViewProviderService view_provider(startup_context.get()); // Add our ViewProvider service to the outgoing services. startup_context->outgoing().AddPublicService<fuchsia::ui::app::ViewProvider>( // ** NEW CODE **: Handle ViewProvider request. [&view_provider]( fidl::InterfaceRequest<fuchsia::ui::app::ViewProvider> request) { view_provider.HandleViewProviderRequest(std::move(request)); }); loop.Run(); return 0; }
On Fuchsia, a component creates a UI by communicating to the graphics compositor, Scenic, over a FIDL protocol. More specifically, the component uses a Session
protocol, which allows a client to enqueue a list of Commands
that can create and modify objects called Resources
. Resources
describes a variety of types which includes nodes, materials, textures, and shapes, among others.
A client creates a scene graph by creating and modifying the Resources
.
The root of a client‘s scene graph is called a View
. A View
also has an associated size, which we will talk about later. First, however, let’s create a class BouncingBallView
which creates a Session
and uses that to create a View
.
class BouncingBallView : public fuchsia::ui::scenic::SessionListener { public: BouncingBallView(component::StartupContext* startup_context, zx::eventpair view_token) : session_listener_binding_(this) { // Connect to Scenic. fuchsia::ui::scenic::ScenicPtr scenic = startup_context ->ConnectToEnvironmentService<fuchsia::ui::scenic::Scenic>(); // Create a Scenic Session and a Scenic SessionListener. scenic->CreateSession(session_.NewRequest(), session_listener_binding_.NewBinding()); InitializeScene(std::move(view_token)); }
In InitializeScene
, we create a list of Commands
, then Enqueue
them. However, the commands are not applied until Present
is called. Present
takes a presentation time when the commands should be applied; it is acceptable to call it with a time of 0
but subsequent calls to Present
should use presentation times based on the PresentationInfo
we receive from Scenic.
static void PushCommand(std::vector<fuchsia::ui::scenic::Command>* cmds, fuchsia::ui::gfx::Command cmd) { // Wrap the gfx::Command in a scenic::Command, then push it. cmds->push_back(scenic::NewCommand(std::move(cmd))); } void InitializeScene(zx::eventpair view_token) { // Build up a list of commands we will send over our Scenic Session. std::vector<fuchsia::ui::scenic::Command> cmds; // View: Use |view_token| to create a View in the Session. PushCommand(&cmds, scenic::NewCreateViewCmd(kViewId, std::move(view_token), "bouncing_ball_view")); session_->Enqueue(std::move(cmds)); // Apply all the commands we've enqueued by calling Present. For this first // frame we call Present with a presentation_time = 0 which means it the // commands should be applied immediately. For future frames, we'll use the // timing information we receive to have precise presentation times. session_->Present(0, {}, {}, [this](fuchsia::images::PresentationInfo info) { // This is the callback after our changes are presented. // TODO: Render a new frame }); }
Once we've created a View
resource, we can now attach our UI to it.
We attach a EntityNode
to it, and to that, we attach ShapeNodes
. We also create Materials
for the ShapeNodes
.
Note, however, that this code is not sufficient to draw a UI. We also need to attach a Shape
(which represents the geometry to render) to a ShapeNode
for it to render. However, we don't yet know the size of our UI and therefore we postpone creating Shapes
until we do, in the next section.
Here, we create a node for a pink background and for a purple circle we will draw on top of it.
void InitializeScene(zx::eventpair view_token) { // Build up a list of commands we will send over our Scenic Session. std::vector<fuchsia::ui::scenic::Command> cmds; // View: Use |view_token| to create a View in the Session. PushCommand(&cmds, scenic::NewCreateViewCmd(kViewId, std::move(view_token), "bouncing_circle_view")); // Root Node. PushCommand(&cmds, scenic::NewCreateEntityNodeCmd(kRootNodeId)); PushCommand(&cmds, scenic::NewAddChildCmd(kViewId, kRootNodeId)); // Background Material. PushCommand(&cmds, scenic::NewCreateMaterialCmd(kBgMaterialId)); PushCommand(&cmds, scenic::NewSetColorCmd(kBgMaterialId, 0xf5, 0x00, 0x57, 0xff)); // Pink A400 // Background ShapeNode. PushCommand(&cmds, scenic::NewCreateShapeNodeCmd(kBgNodeId)); PushCommand(&cmds, scenic::NewSetMaterialCmd(kBgNodeId, kBgMaterialId)); PushCommand(&cmds, scenic::NewAddChildCmd(kRootNodeId, kBgNodeId)); // Circle's Material. PushCommand(&cmds, scenic::NewCreateMaterialCmd(kCircleMaterialId)); PushCommand(&cmds, scenic::NewSetColorCmd(kCircleMaterialId, 0x67, 0x3a, 0xb7, 0xff)); // Deep Purple 500 // Circle's ShapeNode. PushCommand(&cmds, scenic::NewCreateShapeNodeCmd(kCircleNodeId)); PushCommand(&cmds, scenic::NewSetMaterialCmd(kCircleNodeId, kCircleMaterialId)); PushCommand(&cmds, scenic::NewAddChildCmd(kRootNodeId, kCircleNodeId)); session_->Enqueue(std::move(cmds)); // Apply all the commands we've enqueued by calling Present. For this first // frame we call Present with a presentation_time = 0 which means it the // commands should be applied immediately. For future frames, we'll use the // timing information we receive to have precise presentation times. session_->Present(0, {}, {}, [this](fuchsia::images::PresentationInfo info) { OnPresent(std::move(info)); }); }
When we created our Session, we also created a binding to a SessionListener
. SessionListener
is how we get events from Scenic, including size change events (sent as ViewProperties) and input events.
For now, we only process ViewPropertiesChanged events. We get our size, and then create Shapes
(in this case, Rectangle
for the background and Circle
for the ball) for the two nodes in our scene.
// Note: we implement the SessionListener protocol class BouncingBallView : public fuchsia::ui::scenic::SessionListener { ... static bool IsViewPropertiesChangedEvent( const fuchsia::ui::scenic::Event& event) { return event.Which() == fuchsia::ui::scenic::Event::Tag::kGfx && event.gfx().Which() == fuchsia::ui::gfx::Event::Tag::kViewPropertiesChanged; } // |fuchsia::ui::scenic::SessionListener| void OnScenicEvent(std::vector<fuchsia::ui::scenic::Event> events) override { for (auto& event : events) { if (IsViewPropertiesChangedEvent(event)) { OnViewPropertiesChanged( event.gfx().view_properties_changed().properties); } else { // Unhandled event. } } } void OnViewPropertiesChanged(fuchsia::ui::gfx::ViewProperties vp) { view_width_ = (vp.bounding_box.max.x - vp.inset_from_max.x) - (vp.bounding_box.min.x + vp.inset_from_min.x); view_height_ = (vp.bounding_box.max.y - vp.inset_from_max.y) - (vp.bounding_box.min.y + vp.inset_from_min.y); // Position is relative to the View's origin system. const float center_x = view_width_ * .5f; const float center_y = view_height_ * .5f; // Build up a list of commands we will send over our Scenic Session. std::vector<fuchsia::ui::scenic::Command> cmds; // Background Shape. const int bg_shape_id = new_resource_id_++; PushCommand(&cmds, scenic::NewCreateRectangleCmd(bg_shape_id, view_width_, view_height_)); PushCommand(&cmds, scenic::NewSetShapeCmd(kBgNodeId, bg_shape_id)); // We release the Shape Resource here, but it continues to stay alive in // Scenic because it's being referenced by background ShapeNode (i.e. the // one with id kBgNodeId). However, we no longer have a way to reference it. // // Once the background ShapeNode no longer references this shape, because a // new Shape was set on it, this Shape will be destroyed internally in // Scenic. PushCommand(&cmds, scenic::NewReleaseResourceCmd(bg_shape_id)); // Translate the background node. constexpr float kBackgroundElevation = 0.f; PushCommand(&cmds, scenic::NewSetTranslationCmd( kBgNodeId, (float[]){center_x, center_y, -kBackgroundElevation})); // Circle Shape. circle_radius_ = std::min(view_width_, view_height_) * .1f; const int circle_shape_id = new_resource_id_++; PushCommand(&cmds, scenic::NewCreateCircleCmd(circle_shape_id, circle_radius_)); PushCommand(&cmds, scenic::NewSetShapeCmd(kCircleNodeId, circle_shape_id)); // We release the Shape Resource here, but it continues to stay alive in // Scenic because it's being referenced by circle's ShapeNode (i.e. the one // with id kCircleNodeId). However, we no longer have a way to reference it. // // Once the background ShapeNode no longer references this shape, because a // new Shape was set on it, this Shape will be destroyed internally in // Scenic. PushCommand(&cmds, scenic::NewReleaseResourceCmd(circle_shape_id)); session_->Enqueue(std::move(cmds)); // The commands won't actually get committed until Session.Present() is // called. However, since we're animating every frame, in this case we can // assume Present() will be called shortly. } ... }
If we want to push new content, we send more Commands
and call Present
. However, we should use the information we received in PresentationInfo
to determine what the next presentation time will be. In this example, we continually request new frames to animate the circle.
void InitializeScene(zx::eventpair view_token) { // For the first call to present, use presentation_time = 0 since we don't // haven't gotten any timing information yet. session_->Present(0, {}, {}, [this](fuchsia::images::PresentationInfo info) { // ** NEW CODE **: Call |OnPresent| here with the // PresentationInfo. OnPresent(std::move(info)); }); } ... void OnPresent(fuchsia::images::PresentationInfo presentation_info) { uint64_t presentation_time = presentation_info.presentation_time; constexpr float kSecondsPerNanosecond = .000'000'001f; float t = (presentation_time - last_presentation_time_) * kSecondsPerNanosecond; if (last_presentation_time_ == 0) { t = 0; } last_presentation_time_ = presentation_time; std::vector<fuchsia::ui::scenic::Command> cmds; UpdateCirclePosition(t); const float circle_pos_x_absolute = circle_pos_x_ * view_width_; const float circle_pos_y_absolute = circle_pos_y_ * view_height_ - circle_radius_; // Translate the circle's node. constexpr float kCircleElevation = 8.f; PushCommand(&cmds, scenic::NewSetTranslationCmd( kCircleNodeId, (float[]){circle_pos_x_absolute, circle_pos_y_absolute, -kCircleElevation})); session_->Enqueue(std::move(cmds)); zx_time_t next_presentation_time = presentation_info.presentation_time + presentation_info.presentation_interval; session_->Present(next_presentation_time, {}, {}, [this](fuchsia::images::PresentationInfo info) { OnPresent(std::move(info)); }); }
We can also get input events (pointer events, keyboard events) from Scenic by listening to Session events. For example, we can listen for a touch down and then use that in the animation code to reset the ball's starting position:
// |fuchsia::ui::scenic::SessionListener| void OnScenicEvent(std::vector<fuchsia::ui::scenic::Event> events) override { for (auto& event : events) { if (IsViewPropertiesChangedEvent(event)) { OnViewPropertiesChanged( event.gfx().view_properties_changed().properties); } else if (IsPointerDownEvent(event)) { // *** NEW CODE *** pointer_down_ = true; pointer_id_ = event.input().pointer().pointer_id; } else if (IsPointerUpEvent(event)) { // *** NEW CODE *** if (pointer_id_ == event.input().pointer().pointer_id) { pointer_down_ = false; } } else { // Unhandled event. } } }
Congratulations! You've learned how to create a component that can display some cool content! Here are some next steps:
BaseView
, which is a class that helps avoid some of the boilerplate we wrote here.Resources
, rather than using Commands
directly like we have here.