Bringing LVGL to Embedded Swift

Link to demo on YouTube

The demo is running the following code:

@main
struct Main {
    static func main() {
        //var device: Device = Device()
        stdio_init_all()
        System_Init()
        LCD_Init(D2U_L2R, 1000)
        TP_Init(D2U_L2R)
        TP_GetAdFac()

        var ctr: Int = 5

        while ctr != 0 {
            print("Delaying for console access...")
            sleep_ms(1000)
            ctr -= 1
        }

        init_atomic_ops() // Custom implementation of atomic ops for RP2040 to make stuff work
        lcd_lvgl_init() // Initialize display and inform LVGL about it

        var myCounter: Int = 0

        let label = LVGLLabel("", alignment: .bottomMid)
        var button = LVGLButton("Click Me", eventType: .pressed) { event in
            if let event = event, event.eventCode == .pressed {
                myCounter += 1
                label.setText("You clicked the button \(myCounter) times")
            }
        }

        let _ = LVGLButton("Delete", alignment: .leftMid) { event in
            if let event = event, event.eventCode == .pressed {
                print("Deleting button")
                if (button.exists()) {
                    button.delete()
                } else {
                    label.setText("Button already deleted!")
                }
            }
        }

        let _ = LVGLSlider("", alignment: .topMid, yOffset: 50)

        let _ = LVGLSwitch(alignment: .rightMid)

        while true {
            lv_timer_handler()
            sleep_ms(20)
        }
    }
}

Where, lcd_lvgl_init is defined as

void lcd_lvgl_init(void) {
  lv_init();
  display = lv_display_create(480, 320);
  if (display == NULL) {
    printf("Failed to create display\n");
  } else {
    printf("Created display successfully\n");
  }
  lv_display_set_buffers(display, buf1, NULL, sizeof(buf1),
                         LV_DISPLAY_RENDER_MODE_PARTIAL);
  lv_display_set_flush_cb(display, my_disp_flush);

  touch_driver_init();
  printf("Touch driver setup done\n");

  // Set up the tick interface
  lv_tick_set_cb(my_tick_get_cb);
  printf("Callback setup done\n");
}

LVGL Stuff

Here are some snippets to show how I am writing the library:

Enums

enum Color: UInt32 {
    case blue = 0x003a57
    case white = 0xffffff
    case red = 0xff0000
    case green = 0x00ff00
    case black = 0x000000

    func toHex() -> UInt32 {
        return self.rawValue
    }
}

enum LVAlignment: UInt8 {
    case `default` = 0
    case topLeft = 1
    case topMid = 2
    case topRight = 3
    case bottomLeft = 4
    case bottomMid = 5
    case bottomRight = 6
    case leftMid = 7
    case rightMid = 8
    case center = 9
    case outTopLeft = 10
    case outTopMid = 11
    case outTopRight = 12
    case outBottomLeft = 13
    case outBottomMid = 14
    case outBottomRight = 15
    case outLeftTop = 16
    case outLeftMid = 17
    case outLeftBottom = 18
    case outRightTop = 19
    case outRightMid = 20
    case outRightBottom = 21
}

enum LVGLDimension {
    case width, height
}

enum LVEventCode: Int {
    case all = 0
    case pressed
    case pressing
    case pressLost
    case shortClicked
    case longPressed
    case longPressedRepeat
    case clicked
    case released
    case scrollBegin
    case scrollThrowBegin
    case scrollEnd
    case scroll
    case gesture
    case key
    case rotary
    case focused
    case defocused
    case leave
    case hitTest
    case indevReset
    case hoverOver
    case hoverLeave
    case coverCheck
    case refreshExtDrawSize
    case drawMainBegin
    case drawMain
    case drawMainEnd
    case drawPostBegin
    case drawPost
    case drawPostEnd
    case drawTaskAdded
    case valueChanged
    case insert
    case refresh
    case ready
    case cancel
    case create
    case delete
    case childChanged
    case childCreated
    case childDeleted
    case screenUnloadStart
    case screenLoadStart
    case screenLoaded
    case screenUnloaded
    case sizeChanged
    case styleChanged
    case layoutChanged
    case getSelfSize
    case invalidateArea
    case resolutionChanged
    case colorFormatChanged
    case refreshRequest
    case refreshStart
    case refreshReady
    case renderStart
    case renderReady
    case flushStart
    case flushFinish
    case flushWaitStart
    case flushWaitFinish
    case vsync
    case preprocess
    case last = 1000

    func toLVEventCode() -> lv_event_code_t {
        return lv_event_code_t(rawValue: UInt16(self.rawValue))
    }
}

extension UnsafeMutablePointer<lv_event_t> {
    var eventCode: LVEventCode? {
        let rawValue = Int(lv_event_get_code(self).rawValue)
        return LVEventCode(rawValue: rawValue)
    }
}

Protocols and Widgets

protocol LVGLObjectProtocol {
    var pointer: UnsafeMutablePointer<lv_obj_t>? { get set }

    func setPosition(x: Int32)
    func setPosition(y: Int32)
    func setPosition(x: Int32, y: Int32)

    func setSize(width: Int32, height: Int32)
    func setSize(height: Int32)
    func setSize(width: Int32)

    func getDimension(dimension: LVGLDimension) -> Int32
    func getContentDimension(dimension: LVGLDimension) -> Int32
    func getSelfDimension(dimension: LVGLDimension) -> Int32

    func setContentSize(width: Int32, height: Int32)
    func setContentSize(width: Int32)
    func setContentSize(height: Int32)

    func align(alignment: LVAlignment)
    func align(alignment: LVAlignment, xOffset: Int32, yOffset: Int32)
    func center()

    func getParent() -> UnsafeMutablePointer<lv_obj_t>?
    func setParent(parentPointer: UnsafeMutablePointer<lv_obj_t>)

    func setCallback(
        eventType: LVEventCode, _ callback: @escaping (UnsafeMutablePointer<lv_event_t>?) -> Void)
    func removeCallback()

    mutating func delete()
    func exists() -> Bool

    /*
    TODO:
    func refreshSize() -> bool // lv_obj_refr_size
    func setLayout(layout: UInt32) // lv_obj_set_layout
    func isLayoutPositioned() -> bool // lv_obj_is_layout_positioned
    func setLayoutAsDirty() // lv_obj_mark_layout_as_dirty
    func updateLayout() // lv_obj_update_layout
    func align(to: UnsafeMutablePointer<lv_obj_t>? = nil, alignment: LVAlignment, xOffset: Int32, yOffset: Int32) // lv_obj_align_to
    func copyCoords(area to: UnsafeMutablePointer<lv_area_t>) // lv_obj_get_coords

    // Get Coords lv_obj_get_x, lv_obj_get_x2, lv_obj_get_y, lv_obj_get_y2, lv_obj_get_x_aligned, lv_obj_get_y_aligned

    and a few more...

    */
}

extension LVGLObjectProtocol {
    func setPosition(x: Int32) {
        lv_obj_set_x(pointer, x)
    }

    func setPosition(y: Int32) {
        lv_obj_set_y(pointer, y)
    }

    func setPosition(x: Int32, y: Int32) {
        lv_obj_set_pos(pointer, x, y)
    }

    func setSize(width: Int32) {
        lv_obj_set_width(pointer, width)
    }

    func setSize(height: Int32) {
        lv_obj_set_height(pointer, height)
    }

    func setSize(width: Int32, height: Int32) {
        lv_obj_set_size(pointer, width, height)
    }

    func getDimension(dimension: LVGLDimension) -> Int32 {
        switch dimension {
        case .width:
            return lv_obj_get_width(pointer)
        case .height:
            return lv_obj_get_height(pointer)
        }
    }

    func getContentDimension(dimension: LVGLDimension) -> Int32 {
        switch dimension {
        case .width:
            return lv_obj_get_content_width(pointer)
        case .height:
            return lv_obj_get_content_height(pointer)
        }
    }

    func getSelfDimension(dimension: LVGLDimension) -> Int32 {
        switch dimension {
        case .width:
            return lv_obj_get_self_width(pointer)
        case .height:
            return lv_obj_get_self_height(pointer)
        }
    }

    func setContentSize(width: Int32, height: Int32) {
        lv_obj_set_content_width(pointer, width)
        lv_obj_set_content_height(pointer, height)
    }

    func setContentSize(width: Int32) {
        lv_obj_set_content_width(pointer, width)
    }

    func setContentSize(height: Int32) {
        lv_obj_set_content_height(pointer, height)
    }

    func align(alignment: LVAlignment) {
        lv_obj_set_align(pointer, alignment.rawValue)
    }

    func align(alignment: LVAlignment, xOffset: Int32, yOffset: Int32) {
        lv_obj_align(pointer, alignment.rawValue, xOffset, yOffset)
    }

    func center() {
        lv_obj_center(pointer)
    }

    func getParent() -> UnsafeMutablePointer<lv_obj_t>? {
        let parentPointer: UnsafeMutablePointer<lv_obj_t>? = lv_obj_get_parent(pointer)
        return parentPointer
    }

    func setParent(parentPointer: UnsafeMutablePointer<lv_obj_t>) {
        lv_obj_set_parent(pointer, parentPointer)
    }

    func setCallback(
        eventType: LVEventCode,
        _ callback: @escaping (UnsafeMutablePointer<lv_event_t>?) -> Void
    ) {
        callbackStore[UnsafeMutableRawPointer(pointer!)] = callback

        lv_obj_add_event_cb(
            pointer,
            { cCallback($0) },
            eventType.toLVEventCode(),
            nil
        )
    }

    func removeCallback() {
        callbackStore.removeValue(forKey: UnsafeMutableRawPointer(pointer!))
    }

    mutating func delete() {
        if pointer == nil {
            print("Pointer already exists")
            print("This will be a fatal error in the future")
        } else {
            lv_obj_delete(pointer)
            pointer = nil

        }
    }

    func exists() -> Bool {
        if pointer == nil {
            return false
        }
        return true
    }
}

Some Widgets

Label

// MARK: - Label

protocol LVGLLabelProtocol: LVGLObjectProtocol {
    func setText(_ text: String)
}

struct LVGLLabel: LVGLLabelProtocol {
    var pointer: UnsafeMutablePointer<lv_obj_t>?

    init(_ text: String, alignment: LVAlignment = .center, xOffset: Int32 = 0, yOffset: Int32 = 0) {
        guard let label = lv_label_create(lv_screen_active()) else {
            fatalError("Failed to create label")
        }
        self.pointer = label
        setText(text)
        align(alignment: alignment, xOffset: xOffset, yOffset: yOffset)
    }

    func setText(_ text: String) {
        text.withCString { cString in
            lv_label_set_text(pointer, cString)
        }
    }
}

Button

// MARK: - Button

protocol LVGLButtonProtocol: LVGLObjectProtocol {

}

struct LVGLButton: LVGLButtonProtocol {
    public let label: LVGLLabel
    var pointer: UnsafeMutablePointer<lv_obj_t>?

    init(
        _ text: String, alignment: LVAlignment = .center, textColor: Color = .blue,
        xSize: Int32 = 120, ySize: Int32 = 50, xOffset: Int32 = 0, yOffset: Int32 = 0,
        eventType: LVEventCode = .all,
        callback: @escaping (UnsafeMutablePointer<lv_event_t>?) -> Void = { _ in }
    ) {
        guard let button = lv_button_create(lv_screen_active()) else {
            fatalError("Failed to create button")
        }
        self.pointer = button
        self.label = LVGLLabel(
            text, alignment: alignment, xOffset: xOffset, yOffset: yOffset
        )
        self.label.setParent(parentPointer: pointer!)
        align(alignment: alignment, xOffset: xOffset, yOffset: yOffset)
        setSize(width: xSize, height: ySize)
        setCallback(eventType: eventType, callback)

    }

}

Switch

// MARK: - Switch

protocol LVGLSwitchProtocol: LVGLObjectProtocol {
    var checked: Bool { get set}
    func isChecked() -> Bool
    mutating func setChecked(_ to: Bool)
}

struct LVGLSwitch: LVGLSwitchProtocol {
    public var checked: Bool
    var pointer: UnsafeMutablePointer<lv_obj_t>?

    init(_ checked: Bool = false, alignment: LVAlignment = .center, xOffset: Int32 = 0, yOffset: Int32 = 0) {
    guard let mySwitch = lv_switch_create(lv_screen_active()) else {
        fatalError("Failed to create switch")
    }
    self.pointer = mySwitch
    self.checked = checked

    align(alignment: alignment, xOffset: xOffset, yOffset: yOffset)
    setChecked(checked)

    }

    func isChecked() -> Bool {
    return  checked
    }

    mutating func setChecked(_ to: Bool) {
        if (to) {
            lv_obj_add_state(pointer, 1)
        } else {
            lv_obj_remove_state(pointer, 1)
        }
        self.checked = to
    }
}

Callbacks

To handle callbacks for buttons (and any other widgets that support this). I am initializing a global callbacks dictionary

var callbackStore: [UnsafeMutableRawPointer: (UnsafeMutablePointer<lv_event_t>?) -> Void] = [:]

func cCallback(_ event: UnsafeMutablePointer<lv_event_t>?) {
    guard let event = event else { return }
    let target = lv_event_get_target(event)
    if let targetPointer = UnsafeMutableRawPointer(target),
        let callback = callbackStore[targetPointer]
    {
        callback(event)
    }
}

The only reason I am providing these code snippets instead of an entire repository is because my current project repository is way too tightly coupled with my Pico project. Since I haven't really worked with much embedded stuff, I do need ideas regarding the general design for this package:

  • Should I continue with structs, or should I switch to classes?
  • How should the LVGL compilation step be managed? Since LVGL requires you provide lv_conf.h when we are compiling LVGL
  • There has to be a better way to handle callbacks instead of maintaining a global dictionary, right?

Ideally I want to be able to use this same package on macOS/Linux as well since all the user needs to provide is a framebuffer and a flush callback.

9 Likes

Great progress!

If you want the lifetime of the visible graphics "thing" (button, switch, etc..) to be tied to the swift object, we will need to use a class or ~Copyable struct. If you prefer that the object lifetime be manually managed then a plain struct is fine.

Swift typically tries to move away from manual management so I'd shy away from plain structs. Additionally if you want to use LVGL without allocations caused by the language, I think we need to avoid class.

All that said... I think it would be very interesting to see if ~Copyable structs work well as a construction that both provides lifetimes and avoids allocations. If users want to not free the graphics object but throw away their reference to the object they can use the forget keyword.

It would be nice if we could represent this in SwiftPM somehow, but I dont have ideas about that yet.

I dont have concrete ideas on this front yet, but I think playing with the project once published would help me think of things.

1 Like

I was able to write a basic Swift package along with an executable demo https://github.com/navanchauhan/SwiftLVGL

The only requirement for macOS/Linux is SDL2 (I decided to enable SDL2 in the default configuration to make it possible to run the code in a simulator). To override the configuration one can set the environment variable LV_CONF_PATH.

For embedded devices, I have included a script that combines all the Swift files into a single file that can be copied to the project and used along with BridgingHeader.h and CMake

I tried creating non-copyable labels and buttons

public struct Label: ~Copyable {
    private var pointer: UnsafeMutablePointer<lv_obj_t>?
    public init(_ text: String, alignment: LVAlignment = .center) {
        guard let label = lv_label_create(lv_screen_active()) else {
            fatalError("Failed to create label")
        }
        self.pointer = label
        align(alignment: alignment)
        setText(text)
    }
    
    public func align(alignment: LVAlignment) {
        lv_obj_set_align(pointer, alignment.rawValue)
    }
    
    public func setText(_ text: String) {
        text.withCString { cString in
                lv_label_set_text(pointer, cString)
        }
    }
    
    public func setParent(parentPointer: UnsafeMutablePointer<lv_obj_t>) {
      lv_obj_set_parent(pointer, parentPointer)
    }
    
    consuming public func delete() {
        lv_obj_delete(pointer)
    }
    
    deinit {
        print("Deleting")
        if pointer != nil {
            lv_obj_delete(pointer)
        }
    }
}

public struct Button: ~Copyable {
    private var pointer: UnsafeMutablePointer<lv_obj_t>?
    public let label: Label
    
    public init(_ text: String, alignment: LVAlignment = .center, eventType: LVEventCode = .all, callback: @escaping (UnsafeMutablePointer<lv_event_t>?) -> Void = { _ in } ) {
        guard let button = lv_button_create(lv_screen_active()) else {
          fatalError("Failed to create button")
        }
        self.pointer = button
        self.label = Label(
          text,
          alignment: alignment)
        label.setParent(parentPointer: pointer!)
        align(alignment: alignment)
        setCallback(eventType: eventType, callback: callback)
    }
    
    public func align(alignment: LVAlignment) {
        lv_obj_set_align(pointer, alignment.rawValue)
    }
    
    public func setCallback(
      eventType: LVEventCode,
      callback: @escaping (UnsafeMutablePointer<lv_event_t>?) -> Void
    ) {
      callbackStore[UnsafeMutableRawPointer(pointer!)] = callback

      lv_obj_add_event_cb(
        pointer,
        { cCallback($0) },
        eventType.toLVEventCode(),
        nil
      )
    }
    
    consuming public func delete() {
        // Delete
    }
    
    deinit {
        if pointer != nil {
            lv_obj_delete(pointer) // LVGL automatically deletes the children
        }
    }
}

But the main issue with this strategy is that now we cannot delete a widget through an escaping closure

let label: Label = Label("My Label", alignment: .bottomMid)

let anotherButton = Button("Delete label", alignment: .bottomRight, eventType: .pressed) { event in
    label.delete() // Noncopyable 'label' cannot be consumed when captured by an escaping closure
}

I am not sure if SE-0427 adds support for Optionals or if it just lays the groundwork for Optionals to be able to support non-copyable.

Also, I am not sure what you are referring to by the forget keyword

1 Like

Maybe @Joe_Groff has ideas on this front?

Sorry I probably wasn't very clear and I mistakenly forgot that this concept is called discard in Swift and not forget.

By using a ~Copyable struct, we are able to tie an lv_object's lifetime to the lifetime of a rendered component, e.g. when the lv_object goes out of scope the component will no longer be rendered.

If a user wants to exit the scope but still render the component, they can "leak" the object using the discard keyword preventing the deinit from running. swift-evolution/proposals/0390-noncopyable-structs-and-enums.md at main · swiftlang/swift-evolution · GitHub

3 Likes

Not sure if this is useful for embedded Swift, but just FYI:

1 Like

I think we need to wait until we can use noncopyable structs with generic type 'Optional' since that will let us do stuff like

let label: Label?

let anotherButton = Button("Delete", alignment: .bottomRight, eventType: .pressed) { event in
    guard let label = label else { return }
    label.delete()
}

For now, I will go ahead and complete the wrapper to have feature parity with LVGL using structs with a manual .delete(), and then look into building a SwiftUI like library with this wrapper as its backend.

If we ever come back to the idea of noncopyable structs, it won't be too difficult to modify this

1 Like

Also note that SwiftCrossUI has an experimental backend using LVGLSwift. Probably hasn’t seen a lot of love of late, but could be worth checking out before building yet another SwiftUI clone :slight_smile:

I am definitely aware of SwiftCrossUI, Meta (related to adwaita-swift) and Tokamak (including your experimental LVGL backend for Tokamak) as well.

I am trying to create a dependency free package that can be exported into a single Swift file and used directly in an embedded project.

Relevant xkcd

1 Like

Ha, got it, I’ll shut up now ;)

1 Like

You can't statically consume a capture from a closure, because statically we don't know how many times the closure will execute, and if it was executed more than once then the consume could happen more than once, after the capture had already been destroyed. Swift 6.0 does support Optional noncopyable types, and it also adds a method take() that can be used to dynamically consume values. Optional.take() returns the current value in an Optional variable while setting the variable to nil, allowing you to dynamically consume values by:

var label: Label? = Label("My Label", alignment: .bottomMid)

let anotherButton = Button("Delete label", alignment: .bottomRight, eventType: .pressed) { event in
    label.take()!.delete()
}
4 Likes

That works!

I just want to make sure that the correct way to hand the value back to the original variable is by using consume?

var mylabel: Label? = Label("", alignment: .bottomMid)
var counter: Int = 0

let button = Button("Update Counter") { event in
    if let event = event, event.eventCode == .pressed {
        counter += 1
        guard let label = mylabel.take() else {
            print("label deleted")
            return
        }
        label.setText("You clicked the button \(counter) times")
        mylabel = consume label
    }
}

let anotherButton = Button("Delete label", eventType: .pressed) { _ in
    guard let label = mylabel.take() else { return }
    label.delete()
}

Edit: Only problem is that now I can't think of any way to edit the label of the button through its own callback.

Both of these result in an assertion failure in the SIL check (I am assuming this has to do with how I am trying to borrow this struct, and is not the compiler's fault)



      var testButton: LVGLButton?
      
      /*
      // This Crashes as well
      testButton = LVGLButton("Test", eventType: .pressed) { event in
          guard let btn = testButton.take() else { return }
          btn.label.setText("You Clicked Me")
          testButton = consume btn
      }
       */
      
      testButton = LVGLButton("Test", eventType: .pressed) { _ in
          testButton?.label.setText("You Clicked Me")
      }