--- # Copyright 2025 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. title: Round App UI description: | Details on how to use the Pebble SDK to create layouts specifically for round displays. guide_group: user-interfaces order: 4 related_docs: - Graphics - LayerUpdateProc related_examples: - title: Time Dots url: https://github.com/pebble-examples/time-dots/ - title: Text Flow Techniques url: https://github.com/pebble-examples/text-flow-techniques platforms: - chalk --- > This guide is about creating round apps in code. For advice on designing a > round app, read {% guide_link design-and-interaction/in-the-round %}. With the addition of Pebble Time Round (the Chalk platform) to the Pebble family, developers face a new challenge - circular apps! With this display shape, traditional layouts will not display properly due to the obscuring of the corners. Another potential issue is the increased display resolution. Any UI elements that were not previously centered correctly (or drawn with hardcoded coordinates) will also display incorrectly. However, the Pebble SDK provides additions and functionality to help developers cope with this way of thinking. In many cases, a round display can be an aesthetic advantage. An example of this is the traditional circular dial watchface, which has been emulated on Pebble many times, but also wastes corner space. With a round display, these watchfaces can look better than ever. ![time-dots >{pebble-screenshot,pebble-screenshot--time-round-silver-20}](/images/guides/pebble-apps/display-animations/time-dots.png) ## Detecting Display Shape The first step for any app wishing to correctly support both display shapes is to use the available compiler directives to conditionally create the UI. This can be done as shown below: ```c #if defined(PBL_RECT) printf("This code is run on a rectangular display!"); /* Rectangular UI code */ #elif defined(PBL_ROUND) printf("This code is run on a round display!"); /* Round UI code */ #endif ``` Another approach for single value selection is the ``PBL_IF_RECT_ELSE()`` and ``PBL_IF_ROUND_ELSE()`` macros, which accept two parameters for each of the respective round and rectangular cases. For example, ``PBL_IF_RECT_ELSE()`` will compile the first parameter on a rectangular display, and the second one otherwise: ```c // Conditionally print out the shape of the display printf("This is a %s display!", PBL_IF_RECT_ELSE("rectangular", "round")); ``` ## Circular Drawing In addition to the older ``graphics_draw_circle()`` and ``graphics_fill_circle()`` functions, the Pebble SDK for the chalk platform contains additional functions to help draw shapes better suited for a round display. These include: * ``graphics_draw_arc()`` - Draws a line arc clockwise between two angles within a given ``GRect`` area, where 0° is the top of the circle. * ``graphics_fill_radial()`` - Fills a circle clockwise between two angles within a given ``GRect`` area, with adjustable inner inset radius allowing the creation of 'doughnut-esque' shapes. * ``gpoint_from_polar()`` - Returns a ``GPoint`` object describing a point given by a specified angle within a centered ``GRect``. In the Pebble SDK angles between `0` and `360` degrees are specified as values scaled between `0` and ``TRIG_MAX_ANGLE`` to preserve accuracy and avoid floating point math. These are most commonly used when dealing with drawing circles. To help with this conversion, developers can use the ``DEG_TO_TRIGANGLE()`` macro. An example function to draw the letter 'C' in a yellow color is shown below for use in a ``LayerUpdateProc``. ```c static void draw_letter_c(GRect bounds, GContext *ctx) { GRect frame = grect_inset(bounds, GEdgeInsets(30)); graphics_context_set_fill_color(ctx, GColorYellow); graphics_fill_radial(ctx, frame, GOvalScaleModeFitCircle, 30, DEG_TO_TRIGANGLE(-225), DEG_TO_TRIGANGLE(45)); } ``` This produces the expected result, drawn with a smooth antialiased filled circle arc between the specified angles. ![letter-c >{pebble-screenshot,pebble-screenshot--time-round-silver-20}](/images/guides/pebble-apps/display-animations/letter-c.png) ## Adaptive Layouts With not only a difference in display shape, but also in resolution, it is very important that an app's layout not be created using hardcoded coordinates. Consider the examples below, designed to create a child ``Layer`` to fill the size of the parent layer. ```c // Bad - only works on Aplite and Basalt rectangular displays Layer *layer = layer_create(GRect(0, 0, 144, 168)); // Better - uses the native display size GRect bounds = layer_get_bounds(parent_layer); Layer *layer = layer_create(bounds); ``` Using this style, the child layer will always fill the parent layer, regardless of its actual dimensions. In a similar vein, when working with the Pebble Time Round display it can be important that the layout is centered correctly. A set of layout values that are in the center of the classic 144 x 168 pixel display will not be centered when displayed on a 180 x 180 display. The undesirable effect of this can be seen in the example shown below: ![cut-corners >{pebble-screenshot,pebble-screenshot--time-round-silver-20}](/images/guides/pebble-apps/display-animations/cut-corners.png) By using the technique described above, the layout's ``GRect`` objects can specify their `origin` and `size` as a function of the dimensions of the layer they are drawn into, solving this problem. ![centered >{pebble-screenshot,pebble-screenshot--time-round-silver-20}](/images/guides/pebble-apps/display-animations/centered.png) ## Text Flow and Pagination A chief concern when working with a circular display is the rendering of large amounts of text. As demonstrated by an animation in {% guide_link design-and-interaction/in-the-round#pagination %}, continuous reflowing of text makes it much harder to read. A solution to this problem is to render text while flowing within the constraints of the shape of the display, and to scroll/animate it one page at a time. There are three approaches to this available to developers, which are detailed below. For full examples of each, see the [`text-flow-techniques`](https://github.com/pebble-examples/text-flow-techniques) example app. ### Using TextLayer Additions to the ``TextLayer`` API allow text rendered within it to be automatically flowed according to the curve of the display, and paged correctly when the layer is moved or animated further. After a ``TextLayer`` is created in the usual way, text flow can then be enabled: ```c // Create TextLayer TextLayer *s_text_layer = text_layer_create(bounds); /* other properties set up */ // Add to parent Window layer_add_child(window_layer, text_layer_get_layer(s_text_layer)); // Enable paging and text flow with an inset of 5 pixels text_layer_enable_screen_text_flow_and_paging(s_text_layer, 5); ``` > Note: The ``text_layer_enable_screen_text_flow_and_paging()`` function must be > called **after** the ``TextLayer`` is added to the view heirachy (i.e.: after > using ``layer_add_child()``), or else it will have no effect. An example of two ``TextLayer`` elements flowing their text within the constraints of the display shape is shown below: ![text-flow >{pebble-screenshot,pebble-screenshot--time-round-silver-20}](/images/guides/pebble-apps/display-animations/text-flow.png) ### Using ScrollLayer The ``ScrollLayer`` UI component also contains round-friendly functionality, allowing it to scroll its child ``Layer`` elements in pages of the same height as its frame (usually the size of the parent ``Window``). This allows consuming long content to be a more consistent experience, whether it is text, images, or some other kind of information. ```c // Enable ScrollLayer paging scroll_layer_set_paging(s_scroll_layer, true); ``` When combined with a ``TextLayer`` as the main child layer, it becomes easy to display long pieces of textual content on a round display. The ``TextLayer`` can be set up to handle the reflowing of text to follow the display shape, and the ``ScrollLayer`` handles the paginated scrolling. ```c // Add the TextLayer and ScrollLayer to the view heirachy scroll_layer_add_child(s_scroll_layer, text_layer_get_layer(s_text_layer)); layer_add_child(window_layer, scroll_layer_get_layer(s_scroll_layer)); // Set the ScrollLayer's content size to the total size of the text scroll_layer_set_content_size(s_scroll_layer, text_layer_get_content_size(s_text_layer)); // Enable TextLayer text flow and paging const int inset_size = 2; text_layer_enable_screen_text_flow_and_paging(s_text_layer, inset_size); // Enable ScrollLayer paging scroll_layer_set_paging(s_scroll_layer, true); ``` ### Manual Text Drawing The drawing of text into a [`Graphics Context`](``Drawing Text``) can also be performed with awareness of text flow and paging preferences. This can be used to emulate the behavior of the two previous approaches, but with more flexibility. This approach involves the use of the ``GTextAttributes`` object, which is given to the Graphics API to allow it to flow text and paginate when being animated. When initializing the ``Window`` that will do the drawing: ```c // Create the attributes object used for text rendering GTextAttributes *s_attributes = graphics_text_attributes_create(); // Enable text flow with an inset of 5 pixels graphics_text_attributes_enable_screen_text_flow(s_attributes, 5); // Enable pagination with a fixed reference point and bounds, used for animating graphics_text_attributes_enable_paging(s_attributes, bounds.origin, bounds); ``` When drawing some text in a ``LayerUpdateProc``: ```c static void update_proc(Layer *layer, GContext *ctx) { GRect bounds = layer_get_bounds(layer); // Calculate size of the text to be drawn with current attribute settings GSize text_size = graphics_text_layout_get_content_size_with_attributes( s_sample_text, fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD), bounds, GTextOverflowModeWordWrap, GTextAlignmentCenter, s_attributes ); // Draw the text in this box with the current attribute settings graphics_context_set_text_color(ctx, GColorBlack); graphics_draw_text(ctx, s_sample_text, fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD), GRect(bounds.origin.x, bounds.origin.y, text_size.w, text_size.h), GTextOverflowModeWordWrap, GTextAlignmentCenter, s_attributes ); } ``` Once this setup is complete, the text will display correctly when moved or scrolled via a ``PropertyAnimation``, such as one that moves the ``Layer`` that draws the text upwards, and at the same time extending its height to display subsequent pages. An example animation is shown below: ```c GRect window_bounds = layer_get_bounds(window_get_root_layer(s_main_window)); const int duration_ms = 1000; // Animate the Layer upwards, lengthening it to allow the next page to be drawn GRect start = layer_get_frame(s_layer); GRect finish = GRect(start.origin.x, start.origin.y - window_bounds.size.h, start.size.w, start.size.h * 2); // Create and scedule the PropertyAnimation PropertyAnimation *prop_anim = property_animation_create_layer_frame( s_layer, &start, &finish); Animation *animation = property_animation_get_animation(prop_anim); animation_set_duration(animation, duration_ms); animation_schedule(animation); ``` ## Working With a Circular Framebuffer The traditional rectangular Pebble app framebuffer is a single continuous memory segment that developers could access with ``gbitmap_get_data()``. With a round display, Pebble saves memory by clipping sections of each line of difference between the display area and the rectangle it occupies. The resulting masking pattern looks like this: ![mask](/images/guides/pebble-apps/display-animations/mask.png) > Download this mask by saving the PNG image above, or get it as a > [Photoshop PSD layer](/assets/images/guides/pebble-apps/display-animations/round-mask-layer.psd). This has an important implication - the memory segment of the framebuffer can no longer be accessed using classic `y * row_width + x` formulae. Instead, developers should use the ``gbitmap_get_data_row_info()`` API. When used with a given y coordinate, this will return a ``GBitmapDataRowInfo`` object containing a pointer to the row's data, as well as values for the minumum and maximum visible values of x coordinate on that row. For example: ```c static void round_update_proc(Layer *layer, GContext *ctx) { // Get framebuffer GBitmap *fb = graphics_capture_frame_buffer(ctx); GRect bounds = layer_get_bounds(layer); // Write a value to all visible pixels for(int y = 0; y < bounds.size.h; y++) { // Get the min and max x values for this row GBitmapDataRowInfo info = gbitmap_get_data_row_info(fb, y); // Iterate over visible pixels in that row for(int x = info.min_x; x < info.max_x; x++) { // Set the pixel to black memset(&info.data[x], GColorBlack.argb, 1); } } // Release framebuffer graphics_release_frame_buffer(ctx, fb); } ``` ## Displaying More Content When more content is available than fits on the screen at any one time, the user should be made aware using visual clues. The best way to do this is to use the ``ContentIndicator`` UI component. ![content-indicator >{pebble-screenshot,pebble-screenshot--time-round-silver-20}](/images/guides/design-and-interaction/content-indicator.png) A ``ContentIndicator`` can be obtained in two ways. It can be created from scratch with ``content_indicator_create()`` and manually managed to determine when the arrows should be shown, or a built-in instance can be obtained from a ``ScrollLayer``, as shown below: ```c // Get the ContentIndicator from the ScrollLayer s_indicator = scroll_layer_get_content_indicator(s_scroll_layer); ``` In order to draw the arrows indicating more information in each direction, the ``ContentIndicator`` must be supplied with two new ``Layer`` elements that will be used to do the drawing. These should also be added as children to the main ``Window`` root ``Layer`` such that they are visible on top of all other ``Layer`` elements: ```c static void window_load(Window *window) { Layer *window_layer = window_get_root_layer(window); GRect bounds = layer_get_bounds(window_layer); /* ... */ // Create two Layers to draw the arrows s_indicator_up_layer = layer_create( GRect(0, 0, bounds.size.w, STATUS_BAR_LAYER_HEIGHT)); s_indicator_down_layer = layer_create( GRect(0, bounds.size.h - STATUS_BAR_LAYER_HEIGHT, bounds.size.w, STATUS_BAR_LAYER_HEIGHT)); /* ... */ // Add these Layers as children after all other components to appear below layer_add_child(window_layer, s_indicator_up_layer); layer_add_child(window_layer, s_indicator_down_layer); } ``` Once the indicator ``Layer`` elements have been created, each of the up and down directions for conventional vertical scrolling must be configured with data to control its behavior. Aspects such as the color of the arrows and background, whether or not the arrows time out after being brought into view, and the alignment of the drawn arrow within the ``Layer`` itself are configured with a `const` ``ContentIndicatorConfig`` object when each direction is being configured: ```c // Configure the properties of each indicator const ContentIndicatorConfig up_config = (ContentIndicatorConfig) { .layer = s_indicator_up_layer, .times_out = false, .alignment = GAlignCenter, .colors = { .foreground = GColorBlack, .background = GColorWhite } }; content_indicator_configure_direction(s_indicator, ContentIndicatorDirectionUp, &up_config); const ContentIndicatorConfig down_config = (ContentIndicatorConfig) { .layer = s_indicator_down_layer, .times_out = false, .alignment = GAlignCenter, .colors = { .foreground = GColorBlack, .background = GColorWhite } }; content_indicator_configure_direction(s_indicator, ContentIndicatorDirectionDown, &down_config); ``` Unless the ``ContentIndicator`` has been retrieved from another ``Layer`` type that includes an instance, it should be destroyed along with its parent ``Window``: ```c // Destroy a manually created ContentIndicator content_indicator_destroy(s_indicator); ``` For layouts that use the ``StatusBarLayer``, the ``ContentIndicatorDirectionUp`` `.layer` in the ``ContentIndicatorConfig`` object can be given the status bar's ``Layer`` with ``status_bar_layer_get_layer()``, and the drawing routines for each will be managed automatically.