Files
buzz/mobile/ios/Runner/NativeTabler.swift
klopez4212 b4509dee8b feat(mobile): add Buzzies and refresh agent avatar picker (#8921)
## Summary

Adds Buzzies and refreshes the agent avatar picker with larger previews,
rounded option strips, and clearer category choices. Also moves Agents
above Appearance in the home menu and adds a custom agent icon.

### Related issue

None found.

### Testing

Analysis, 4,138 app tests, package tests, and 121 configured-auth tests
pass. Earlier visual build exercised on Pixel and iOS simulator, with
iPhone review; snapshots below show that build.

| Android · Pixel | iOS · simulator preview |
| --- | --- |
| ![Buzzies in the Android avatar
picker](https://github.com/user-attachments/assets/bf56120c-0128-4683-a0ad-189587a13c8d)
| ![Buzzies in the iOS avatar
picker](https://github.com/user-attachments/assets/63c692f5-822c-4490-883f-36bec8d2be15)
|

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
2026-11-02 17:00:12 +00:00

127 lines
5.2 KiB
Swift

import UIKit
import CoreText
import Flutter
/// Renders the same bundled Tabler font used by Flutter into template images.
/// Legacy symbol keys are kept at the platform-channel boundary only.
enum NativeTabler {
private static let fontName: String? = {
let key = FlutterDartProject.lookupKey(forAsset: "assets/fonts/TablerIcons.ttf")
let appBundle = Bundle(url: Bundle.main.bundleURL.appendingPathComponent("Frameworks/App.framework"))
guard let url = appBundle?.url(forResource: key, withExtension: nil) ?? Bundle.main.url(forResource: key, withExtension: nil),
let provider = CGDataProvider(url: url as CFURL),
let font = CGFont(provider) else { return nil }
CTFontManagerRegisterGraphicsFont(font, nil)
return font.postScriptName as String?
}()
static func image(_ symbol: String, size: CGFloat = 20) -> UIImage? {
if symbol == "agent" { return agentImage(size: size) }
guard let code = glyphs[symbol], let scalar = UnicodeScalar(code),
let name = fontName, let font = UIFont(name: name, size: size) else {
assertionFailure("Missing Tabler icon: \(symbol)")
return nil
}
let text = String(scalar) as NSString
let attributes: [NSAttributedString.Key: Any] = [.font: font, .foregroundColor: UIColor.black]
let measured = text.size(withAttributes: attributes)
return UIGraphicsImageRenderer(size: CGSize(width: size, height: size)).image { _ in
text.draw(at: CGPoint(x: (size - measured.width) / 2, y: (size - measured.height) / 2), withAttributes: attributes)
}.withRenderingMode(.alwaysTemplate)
}
/// Matches Flutter's AgentNavigationIcon and the desktop agent mark.
private static func agentImage(size: CGFloat) -> UIImage {
UIGraphicsImageRenderer(size: CGSize(width: size, height: size)).image { renderer in
renderer.cgContext.scaleBy(x: size / 24, y: size / 24)
UIColor.black.setStroke()
UIColor.black.setFill()
let head = UIBezierPath(roundedRect: CGRect(x: 4.75, y: 7, width: 14.5, height: 13.25), cornerRadius: 4.5)
head.lineWidth = 2
head.stroke()
let lines = UIBezierPath()
lines.lineWidth = 2
lines.lineCapStyle = .round
for (start, end) in [
(CGPoint(x: 12, y: 7), CGPoint(x: 12, y: 2.5)),
(CGPoint(x: 3.25, y: 11.5), CGPoint(x: 3.25, y: 15.75)),
(CGPoint(x: 20.75, y: 11.5), CGPoint(x: 20.75, y: 15.75))
] {
lines.move(to: start)
lines.addLine(to: end)
}
lines.stroke()
for x in [9.0, 15.0] {
UIBezierPath(ovalIn: CGRect(x: x - 1.4, y: 11.85, width: 2.8, height: 2.8)).fill()
}
}.withRenderingMode(.alwaysTemplate)
}
private static let glyphs: [String: UInt32] = [
"arrow.down": 0xea16, // arrow-down
"xmark": 0xeb55, // x
"xmark.circle.fill": 0xea6a, // circle-x
"chevron.backward": 0xea60, // chevron-left
"chevron.left": 0xea60, // chevron-left
"chevron.right": 0xea61, // chevron-right
"camera": 0xea54, // camera
"photo.on.rectangle": 0xfd4a, // images — matches BuzzIcons.images
"photo.on.rectangle.angled": 0xfd4a, // images — matches BuzzIcons.images
"paintpalette": 0xeb01, // palette
"drop": 0xea97, // droplet
"drop.fill": 0xea97, // droplet
"face.smiling": 0xeaf7, // mood-smile
"person": 0xeb4d, // user
"person.crop.circle": 0xeb4d, // user
"arrow.left.arrow.right": 0xf1f4, // arrows-left-right
"person.badge.plus": 0xeb4b, // user-plus
"gearshape": 0xeb20, // settings
"person.2": 0xebf2, // users
"rectangle.stack": 0xeb0a, // photo
"crop": 0xea85, // crop
"arrow.triangle.2.circlepath.camera": 0xee22, // camera-rotate
"circle": 0xea6b, // circle
"circle.fill": 0xea6b, // circle
"sun.max": 0xeb30, // sun
"moon": 0xeaf8, // moon
"ellipsis": 0xea95, // dots
"arrowshape.turn.up.left": 0xeb77, // arrow-back-up
"circle.lefthalf.filled": 0xf4a3, // sun-moon
"magnifyingglass": 0xeb1c, // search
"tray": 0xeac4, // inbox
"sparkles": 0xf6d7, // sparkles
"clock": 0xea70, // clock
"leaf": 0xed4f, // leaf
"fork.knife": 0xeeff, // tools-kitchen-2
"figure.run": 0xec82, // run
"airplane": 0xeb6f, // plane
"lightbulb": 0xea51, // bulb
"heart": 0xeabe, // heart
"flag": 0xeaa6, // flag
"video": 0xed22, // video
"mic": 0xeaf0, // microphone
"doc": 0xeaa4, // file
"plus": 0xeb0b, // plus
"building.2.crop.circle": 0xebf6, // building-community
"workspace": 0xecd7, // cube-3d-sphere
"folder.badge.plus": 0xeaab, // folder-plus
"headphones": 0xeabd, // headphones
"line.3.horizontal.decrease": 0xeaa5, // filter
"square.and.pencil": 0xea98, // edit
"pencil": 0xea98, // edit
"doc.on.doc": 0xea7a, // copy
"trash": 0xeb41, // trash
"pin": 0xec9c, // pin
"pin.slash": 0xed5f, // pinned-off
"square.and.arrow.up": 0xeb21, // share
"link": 0xeade, // link
"checkmark": 0xea5e, // check
"checkmark.circle": 0xea67, // circle-check
"bell.slash": 0xece9, // bell-off
"number": 0xeabc, // hash — matches BuzzIcons.hash
"bubble.left": 0xeaef, // message
"bubble.left.and.bubble.right": 0xeb6c, // messages
"arrow.uturn.backward": 0xeb77, // arrow-back-up
]
}