From 8a447a7555a9484aa0451203fce6e870f10e3051 Mon Sep 17 00:00:00 2001 From: Aspen Schneider Date: Tue, 5 May 2026 12:08:21 +0900 Subject: [PATCH 1/6] BView resize event. --- Sources/Blusher/Core/Color.swift | 4 +++ Sources/Blusher/GUI/BView.swift | 31 +++++++++++++++++++++++- Sources/Blusher/GUI/Window/BWindow.swift | 19 ++++++++++++--- 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/Sources/Blusher/Core/Color.swift b/Sources/Blusher/Core/Color.swift index 78692d5..5513796 100644 --- a/Sources/Blusher/Core/Color.swift +++ b/Sources/Blusher/Core/Color.swift @@ -31,4 +31,8 @@ public extension Color { static var red: Color { Color(r: 1.0, g: 0.0, b: 0.0, a: 1.0) } + + static var transparent: Color { + Color(r: 0.0, g: 0.0, b: 0.0, a: 0.0) + } } diff --git a/Sources/Blusher/GUI/BView.swift b/Sources/Blusher/GUI/BView.swift index 62ce4ab..3032305 100644 --- a/Sources/Blusher/GUI/BView.swift +++ b/Sources/Blusher/GUI/BView.swift @@ -24,6 +24,7 @@ open class BView { private var _pointerPressEventListener: EventListener! private var _pointerReleaseEventListener: EventListener! private var _pointerClickEventListener: EventListener! + private var _resizeEventListener: EventListener! internal var _pointerEnterHandler: ((PointerEvent) -> Void)? = nil internal var _pointerLeaveHandler: ((PointerEvent) -> Void)? = nil @@ -31,6 +32,7 @@ open class BView { internal var _pointerPressHandler: ((PointerEvent) -> Void)? = nil internal var _pointerReleaseHandler: ((PointerEvent) -> Void)? = nil internal var _pointerClickHandler: ((PointerEvent) -> Void)? = nil + internal var _resizeHandler: ((ResizeEvent) -> Void)? = nil internal var cPointer: OpaquePointer? { _sbView @@ -73,7 +75,9 @@ open class BView { return _color } set(newValue) { - if _color == newValue { + // TODO: Optimization. + if newValue == .transparent { + } else if _color == newValue { return } @@ -358,6 +362,17 @@ open class BView { } as EventListener sb_view_add_event_listener(_sbView, SB_EVENT_TYPE_POINTER_CLICK, _pointerClickEventListener, userData) + + // Resize event. + _resizeEventListener = { sbEvent, userData in + if let userData = userData { + let instance = Unmanaged.fromOpaque(userData).takeUnretainedValue() + + instance.callResizeEvent(sbEvent) + } + } as EventListener + sb_view_add_event_listener(_sbView, SB_EVENT_TYPE_RESIZE, + _resizeEventListener, userData) } private func callPointerEnterEvent(_ sbEvent: UnsafeMutablePointer?) { @@ -456,6 +471,16 @@ open class BView { pointerClickEvent(event) } + private func callResizeEvent(_ sbEvent: UnsafeMutablePointer?) { + // TODO: Real values. + print("TODO: BView.resizeEvent values not set.") + let event = ResizeEvent( + oldSize: Size(width: 0.0, height: 0.0), + size: Size(width: 0.0, height: 0.0) + ) + resizeEvent(event) + } + open func pointerEnterEvent(_ event: PointerEvent) { ToplevelStorage._uiSurface = self._surface _pointerEnterHandler?(event) @@ -488,4 +513,8 @@ open class BView { _pointerClickHandler?(event) ToplevelStorage._uiSurface = nil } + + open func resizeEvent(_ event: ResizeEvent) { + _resizeHandler?(event) + } } diff --git a/Sources/Blusher/GUI/Window/BWindow.swift b/Sources/Blusher/GUI/Window/BWindow.swift index 27881a8..64f0729 100644 --- a/Sources/Blusher/GUI/Window/BWindow.swift +++ b/Sources/Blusher/GUI/Window/BWindow.swift @@ -247,7 +247,7 @@ public class BWindowResize: BView { self.cursorShape = _cursorShape - self.color = Color(r: 1.0, g: 0.0, b: 0.0, a: 0.5) + self.color = .transparent } public override func pointerPressEvent(_ event: PointerEvent) { @@ -275,7 +275,7 @@ public class BWindowResize: BView { super.init(surface: window, geometry: Rect(x: 0.0, y: 0.0, width: 1.0, height: 1.0)) - self.color = Color(r: 0.0, g: 1.0, b: 0.0, a: 0.5) + self.color = .transparent // Create edges. _topLeft = Edge(at: .topLeft, in: self) @@ -334,9 +334,22 @@ public class BWindowShadow: BView { set { return } } + private var _shadowInner: BView! + init(_ window: BWindow) { super.init(surface: window, geometry: Rect(x: 0.0, y: 0.0, width: 1.0, height: 1.0)) - self.color = Color(r: 0.0, g: 0.0, b: 0.0, a: 0.5) + _shadowInner = BView( + parent: self, + geometry: Rect( + x: 20.0, y: 20.0, + width: Float(window.surfaceSize.width) - 20.0, + height: Float(window.surfaceSize.height) - 20.0 + ) + ) + _shadowInner.color = Color(r: 0.0, g: 0.0, b: 0.0, a: 0.5) + _shadowInner.addFilter(Blur(radius: 5.0)) + + self.color = .transparent } } From 4643e99129b56ba722e5e37473651694abe418a7 Mon Sep 17 00:00:00 2001 From: Aspen Schneider Date: Tue, 5 May 2026 14:10:00 +0900 Subject: [PATCH 2/6] Add resize event for view. --- Sources/Blusher/Core/Events/Event.swift | 39 ++++++++++++++++++++++++ Sources/Blusher/GUI/BView.swift | 3 ++ Sources/Blusher/GUI/Window/BWindow.swift | 10 ++++-- 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/Sources/Blusher/Core/Events/Event.swift b/Sources/Blusher/Core/Events/Event.swift index ddbe47f..2464c69 100644 --- a/Sources/Blusher/Core/Events/Event.swift +++ b/Sources/Blusher/Core/Events/Event.swift @@ -40,3 +40,42 @@ public class Event { self.type = type } } + +public struct EventHandler { + public final class Handler { + let closure: (T) -> Void + + public init(_ closure: @escaping (T) -> Void) { + self.closure = closure + } + } + + internal var handlers: [Handler] = [] + + public func invoke(_ event: T) { + self.handlers.forEach { + $0.closure(event) + } + } +} + +public func += (_ lhs: inout EventHandler?, _ rhs: EventHandler.Handler) { + if lhs == nil { + lhs = EventHandler() + } + lhs!.handlers.append(rhs) +} + +public func += (_ lhs: inout EventHandler?, _ rhs: @escaping (T) -> Void) { + if lhs == nil { + lhs = EventHandler() + } + lhs!.handlers.append(EventHandler.Handler(rhs)) +} + +public func -= (_ lhs: inout EventHandler?, _ rhs: EventHandler.Handler) { + lhs?.handlers.removeAll { $0 === rhs } + if lhs?.handlers.count == 0 { + lhs = nil + } +} diff --git a/Sources/Blusher/GUI/BView.swift b/Sources/Blusher/GUI/BView.swift index 3032305..b54d875 100644 --- a/Sources/Blusher/GUI/BView.swift +++ b/Sources/Blusher/GUI/BView.swift @@ -34,6 +34,8 @@ open class BView { internal var _pointerClickHandler: ((PointerEvent) -> Void)? = nil internal var _resizeHandler: ((ResizeEvent) -> Void)? = nil + public var onResize: EventHandler? = nil + internal var cPointer: OpaquePointer? { _sbView } @@ -516,5 +518,6 @@ open class BView { open func resizeEvent(_ event: ResizeEvent) { _resizeHandler?(event) + self.onResize?.invoke(event) } } diff --git a/Sources/Blusher/GUI/Window/BWindow.swift b/Sources/Blusher/GUI/Window/BWindow.swift index 64f0729..df53fd9 100644 --- a/Sources/Blusher/GUI/Window/BWindow.swift +++ b/Sources/Blusher/GUI/Window/BWindow.swift @@ -343,12 +343,18 @@ public class BWindowShadow: BView { parent: self, geometry: Rect( x: 20.0, y: 20.0, - width: Float(window.surfaceSize.width) - 20.0, - height: Float(window.surfaceSize.height) - 20.0 + width: Float(window.surfaceSize.width) - 40.0, + height: Float(window.surfaceSize.height) - 40.0 ) ) _shadowInner.color = Color(r: 0.0, g: 0.0, b: 0.0, a: 0.5) _shadowInner.addFilter(Blur(radius: 5.0)) + self.onResize += { [weak self] event in + self?._shadowInner.size = Size( + width: Float(window.surfaceSize.width) - 40.0, + height: Float(window.surfaceSize.height) - 40.0 + ) + } self.color = .transparent } From 8b44216476854f878edecf82b20efe82227acb88 Mon Sep 17 00:00:00 2001 From: Aspen Schneider Date: Thu, 28 May 2026 15:46:20 +0900 Subject: [PATCH 3/6] Sync with Swingby, `Rect.pos` to `Rect.position`. --- Sources/Blusher/Core/Rect.swift | 16 ++++++++-------- Sources/Blusher/GUI/BSurface.swift | 14 +++++++++----- Sources/Blusher/GUI/BView.swift | 14 +++++++------- 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/Sources/Blusher/Core/Rect.swift b/Sources/Blusher/Core/Rect.swift index 4e86286..4e63c00 100644 --- a/Sources/Blusher/Core/Rect.swift +++ b/Sources/Blusher/Core/Rect.swift @@ -1,23 +1,23 @@ public struct Rect: Equatable { - public var pos: Point = Point(x: 0.0, y: 0.0) + public var position: Point = Point(x: 0.0, y: 0.0) public var size: Size = Size(width: 0.0, height: 0.0) public init(x: Float, y: Float, width: Float, height: Float) { - pos.x = x - pos.y = y + position.x = x + position.y = y size.width = width size.height = height } public var x: Float { get { - return pos.x + return position.x } } public var y: Float { get { - return pos.y + return position.y } } @@ -41,12 +41,12 @@ extension Rect: CustomStringConvertible { } public struct RectI: Equatable { - public var pos: PointI = PointI(x: 0, y: 0) + public var position: PointI = PointI(x: 0, y: 0) public var size: SizeI = SizeI(width: 0, height: 0) public init(x: Int64, y: Int64, width: UInt64, height: UInt64) { - pos.x = x - pos.y = y + position.x = x + position.y = y size.width = width size.height = height } diff --git a/Sources/Blusher/GUI/BSurface.swift b/Sources/Blusher/GUI/BSurface.swift index 208310f..f169d22 100644 --- a/Sources/Blusher/GUI/BSurface.swift +++ b/Sources/Blusher/GUI/BSurface.swift @@ -99,7 +99,9 @@ public class BSurface { if _visible { var sbRect = sb_rect_t( - pos: sb_point_t(x: Float(newValue.pos.x), y: Float(newValue.pos.y)), + position: sb_point_t( + x: Float(newValue.position.x), y: Float(newValue.position.y) + ), size: sb_size_t(width: Float(newValue.size.width), height: Float(newValue.size.height)) ) @@ -117,7 +119,9 @@ public class BSurface { } set { var sbRect = sb_rect_t( - pos: sb_point_t(x: Float(newValue.pos.x), y: Float(newValue.pos.y)), + position: sb_point_t( + x: Float(newValue.position.x), y: Float(newValue.position.y) + ), size: sb_size_t(width: Float(newValue.size.width), height: Float(newValue.size.height)) ) @@ -198,9 +202,9 @@ public class BSurface { // wmGeometry must set after .show() called. if _visible && _wmGeometry != nil { var sbRect = sb_rect_t( - pos: sb_point_t( - x: Float(_wmGeometry!.pos.x), - y: Float(_wmGeometry!.pos.y) + position: sb_point_t( + x: Float(_wmGeometry!.position.x), + y: Float(_wmGeometry!.position.y) ), size: sb_size_t( width: Float(_wmGeometry!.size.width), diff --git a/Sources/Blusher/GUI/BView.swift b/Sources/Blusher/GUI/BView.swift index b54d875..488a944 100644 --- a/Sources/Blusher/GUI/BView.swift +++ b/Sources/Blusher/GUI/BView.swift @@ -116,8 +116,8 @@ open class BView { public var geometry: Rect { get { let sbRect = sb_view_geometry(_sbView) - let x = sbRect!.pointee.pos.x - let y = sbRect!.pointee.pos.y + let x = sbRect!.pointee.position.x + let y = sbRect!.pointee.position.y let width = sbRect!.pointee.size.width let height = sbRect!.pointee.size.height @@ -131,7 +131,7 @@ open class BView { _geometry = newValue var sbRect = sb_rect_t( - pos: sb_point_t(x: newValue.pos.x, y: newValue.pos.y), + position: sb_point_t(x: newValue.position.x, y: newValue.position.y), size: sb_size_t(width: newValue.size.width, height: newValue.size.height) ) @@ -145,7 +145,7 @@ open class BView { public var position: Point { get { - return Point(x: geometry.pos.x, y: geometry.pos.y) + return Point(x: geometry.position.x, y: geometry.position.y) } set { let geo = Rect( @@ -162,7 +162,7 @@ open class BView { } set(newValue) { let geo = Rect( - x: geometry.pos.x, y: geometry.pos.y, + x: geometry.position.x, y: geometry.position.y, width: newValue.width, height: newValue.height ) geometry = geo @@ -218,7 +218,7 @@ open class BView { public init(parent: BView, geometry: Rect) { let sbParent = parent._sbView var sbRect = sb_rect_t( - pos: sb_point_t(x: geometry.x, y: geometry.y), + position: sb_point_t(x: geometry.x, y: geometry.y), size: sb_size_t(width: geometry.width, height: geometry.height) ) @@ -237,7 +237,7 @@ open class BView { public init(surface: BSurface, geometry: Rect) { var sbRect = sb_rect_t( - pos: sb_point_t(x: geometry.x, y: geometry.y), + position: sb_point_t(x: geometry.x, y: geometry.y), size: sb_size_t(width: geometry.width, height: geometry.height) ) From 3736da2d10c05e6907595a422a4a7aa7c0117313 Mon Sep 17 00:00:00 2001 From: Aspen Schneider Date: Fri, 29 May 2026 12:09:37 +0900 Subject: [PATCH 4/6] Canvas view test. --- Sources/Blusher/Core/Canvas.swift | 51 +++++++++++++++++++ Sources/Blusher/Core/Events/Event.swift | 1 + Sources/Blusher/Core/Paint.swift | 10 ++++ Sources/Blusher/GUI/BView.swift | 34 +++++++++++++ Sources/Blusher/GUI/Window/BWindow.swift | 27 ++++++++++ .../Sources/Example/Example.swift | 1 - 6 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 Sources/Blusher/Core/Canvas.swift create mode 100644 Sources/Blusher/Core/Paint.swift diff --git a/Sources/Blusher/Core/Canvas.swift b/Sources/Blusher/Core/Canvas.swift new file mode 100644 index 0000000..25450fb --- /dev/null +++ b/Sources/Blusher/Core/Canvas.swift @@ -0,0 +1,51 @@ +@_implementationOnly import Swingby + +func colorToSbColor(_ color: Color) -> sb_color_t { + let sb_color_t = sb_color_t(r: color.r, g: color.g, b: color.b, a: color.a) + + return sb_color_t +} + +func paintToSbPaint(_ paint: Paint) -> sb_paint_t { + let sbPaint = sb_paint_t( + fill_color: colorToSbColor(paint.fillColor), + stroke_color: colorToSbColor(paint.strokeColor), + stroke_width: paint.strokeWidth + ) + + return sbPaint +} + +public class Canvas { + private var _sbCanvas: OpaquePointer? = nil + + internal init(_ sbCanvas: OpaquePointer) { + _sbCanvas = sbCanvas + } + + public func drawRect(_ rect: Rect, _ paint: Paint) { + var sbRect = sb_rect_t( + position: sb_point_t(x: rect.x, y: rect.y), + size: sb_size_t(width: rect.width, height: rect.height) + ) + var sbPaint = paintToSbPaint(paint) + + if let sbCanvas = _sbCanvas { + sb_canvas_draw_rect(sbCanvas, &sbRect, &sbPaint) + } + } + + public func drawLine(_ p1: Point, _ p2: Point, _ paint: Paint) { + if let sbCanvas = _sbCanvas { + var sbP1 = sb_point_t(x: p1.x, y: p1.y) + var sbP2 = sb_point_t(x: p2.x, y: p2.y) + var sbPaint = paintToSbPaint(paint) + + sb_canvas_draw_line(sbCanvas, &sbP1, &sbP2, &sbPaint) + } + } + + public func drawLine(_ x1: Float, _ y1: Float, _ x2: Float, _ y2: Float, _ paint: Paint) { + self.drawLine(Point(x: x1, y: y1), Point(x: x2, y: y2), paint) + } +} diff --git a/Sources/Blusher/Core/Events/Event.swift b/Sources/Blusher/Core/Events/Event.swift index 2464c69..c48c00f 100644 --- a/Sources/Blusher/Core/Events/Event.swift +++ b/Sources/Blusher/Core/Events/Event.swift @@ -8,6 +8,7 @@ public enum EventType { case pointerPress case pointerRelease case pointerClick + case paint case resize case preferredScale } diff --git a/Sources/Blusher/Core/Paint.swift b/Sources/Blusher/Core/Paint.swift new file mode 100644 index 0000000..c3e4bb1 --- /dev/null +++ b/Sources/Blusher/Core/Paint.swift @@ -0,0 +1,10 @@ +@_implementationOnly import Swingby + +public struct Paint { + public var fillColor: Color = .transparent + public var strokeColor: Color = .black + public var strokeWidth: Float = 0.0 + + public init() { + } +} diff --git a/Sources/Blusher/GUI/BView.swift b/Sources/Blusher/GUI/BView.swift index 488a944..37976d8 100644 --- a/Sources/Blusher/GUI/BView.swift +++ b/Sources/Blusher/GUI/BView.swift @@ -3,6 +3,8 @@ public enum ViewRenderType { case singleColor case image + case text + case canvas } open class BView { @@ -25,6 +27,7 @@ open class BView { private var _pointerReleaseEventListener: EventListener! private var _pointerClickEventListener: EventListener! private var _resizeEventListener: EventListener! + private var _paintEventListener: EventListener! internal var _pointerEnterHandler: ((PointerEvent) -> Void)? = nil internal var _pointerLeaveHandler: ((PointerEvent) -> Void)? = nil @@ -33,6 +36,7 @@ open class BView { internal var _pointerReleaseHandler: ((PointerEvent) -> Void)? = nil internal var _pointerClickHandler: ((PointerEvent) -> Void)? = nil internal var _resizeHandler: ((ResizeEvent) -> Void)? = nil + internal var _paintHandler: ((Event) -> Void)? = nil public var onResize: EventHandler? = nil @@ -67,6 +71,8 @@ open class BView { let sbType = switch _renderType { case .singleColor: SB_VIEW_RENDER_TYPE_SINGLE_COLOR case .image: SB_VIEW_RENDER_TYPE_IMAGE + case .text: SB_VIEW_RENDER_TYPE_GLYPHS + case .canvas: SB_VIEW_RENDER_TYPE_CANVAS } sb_view_set_render_type(_sbView, sbType) } @@ -113,6 +119,14 @@ open class BView { } } + public var canvas: Canvas? { + if let sbCanvas = sb_view_canvas(_sbView) { + let canvas = Canvas(sbCanvas) + return canvas + } + return nil + } + public var geometry: Rect { get { let sbRect = sb_view_geometry(_sbView) @@ -375,6 +389,17 @@ open class BView { } as EventListener sb_view_add_event_listener(_sbView, SB_EVENT_TYPE_RESIZE, _resizeEventListener, userData) + + // Paint event. + _paintEventListener = { sbEvent, userData in + if let userData = userData { + let instance = Unmanaged.fromOpaque(userData).takeUnretainedValue() + + instance.callPaintEvent(sbEvent) + } + } + sb_view_add_event_listener(_sbView, SB_EVENT_TYPE_PAINT, + _paintEventListener, userData) } private func callPointerEnterEvent(_ sbEvent: UnsafeMutablePointer?) { @@ -483,6 +508,11 @@ open class BView { resizeEvent(event) } + private func callPaintEvent(_ sbEvent: UnsafeMutablePointer?) { + let event = Event(of: .paint) + paintEvent(event) + } + open func pointerEnterEvent(_ event: PointerEvent) { ToplevelStorage._uiSurface = self._surface _pointerEnterHandler?(event) @@ -520,4 +550,8 @@ open class BView { _resizeHandler?(event) self.onResize?.invoke(event) } + + open func paintEvent(_ event: Event) { + _paintHandler?(event) + } } diff --git a/Sources/Blusher/GUI/Window/BWindow.swift b/Sources/Blusher/GUI/Window/BWindow.swift index df53fd9..6660fc8 100644 --- a/Sources/Blusher/GUI/Window/BWindow.swift +++ b/Sources/Blusher/GUI/Window/BWindow.swift @@ -167,6 +167,29 @@ public class BTitleBar: BView { } } + public class Caption: BView { + init(_ titleBar: BTitleBar) { + let rect = Rect(x: 100.0, y: 0.0, width: 100.0, height: BTitleBar.thickness) + super.init(parent: titleBar, geometry: rect) + + self.renderType = .canvas + } + + public override func paintEvent(_ event: Event) { + var paint = Paint() + paint.strokeWidth = 2.0 + paint.strokeColor = .black + + self.canvas?.drawLine( + Point(x: 0.0, y: 10.0), + Point(x: 30.0, y: 10.0), + paint + ) + + super.paintEvent(event) + } + } + public static var thickness: Float { 30.0 } @@ -178,6 +201,8 @@ public class BTitleBar: BView { private var _minimizeButton: Button! private var _maximizeOrRestoreButton: Button! + private var _caption: Caption! + init(_ window: BWindow) { _window = window @@ -193,6 +218,8 @@ public class BTitleBar: BView { _minimizeButton.position = Point(x: 40.0, y: 3.0) _maximizeOrRestoreButton.position = Point(x: 70.0, y: 3.0) + _caption = Caption(self) + self.color = Color(r: 0.5, g: 0.5, b: 0.5, a: 1.0) self.radius = Radius(topLeft: 8.0, topRight: 8.0, bottomRight: 0.0, bottomLeft: 0.0) } diff --git a/examples/07-image-spm/Sources/Example/Example.swift b/examples/07-image-spm/Sources/Example/Example.swift index d006c0a..3314131 100644 --- a/examples/07-image-spm/Sources/Example/Example.swift +++ b/examples/07-image-spm/Sources/Example/Example.swift @@ -38,4 +38,3 @@ public struct Program { let _ = app.exec() } } - From 73294bdf56ff9353fc595752a79ceafac1e2dad7 Mon Sep 17 00:00:00 2001 From: Aspen Schneider Date: Sun, 31 May 2026 09:59:37 +0900 Subject: [PATCH 5/6] Temporary `Bytes` to `String` encode function. The API will be change as soon as. --- Sources/Blusher/Core/Utilities/Bytes.swift | 18 ++++++++++++++++++ Tests/blusherTests/blusherTests.swift | 9 +++++++++ Tests/blusherTests/valid-utf8.txt | 1 + 3 files changed, 28 insertions(+) create mode 100644 Tests/blusherTests/valid-utf8.txt diff --git a/Sources/Blusher/Core/Utilities/Bytes.swift b/Sources/Blusher/Core/Utilities/Bytes.swift index 94174cf..2b311c0 100644 --- a/Sources/Blusher/Core/Utilities/Bytes.swift +++ b/Sources/Blusher/Core/Utilities/Bytes.swift @@ -34,4 +34,22 @@ public class Bytes { _ptr?.deallocate() } } + + public func decode(encoding: UTF8) -> String? { + var codec = encoding + var result = "" + result.reserveCapacity(Int(_len)) + + var iter = _ptr!.makeIterator() + while true { + switch codec.decode(&iter) { + case .scalarValue(let scalar): + result.unicodeScalars.append(scalar) + case .emptyInput: + return result + case .error: + return nil + } + } + } } diff --git a/Tests/blusherTests/blusherTests.swift b/Tests/blusherTests/blusherTests.swift index f045979..9905812 100644 --- a/Tests/blusherTests/blusherTests.swift +++ b/Tests/blusherTests/blusherTests.swift @@ -26,3 +26,12 @@ import Testing let _ = bytes } + +@Test func bytes() async throws { + let file = FileSystem.File.open("Tests/blusherTests/valid-utf8.txt", "rb") + let bytes = file.readAll() + file.close() + + let decoded = bytes.decode(encoding: UTF8()) + #expect(decoded == "こんにちは!\n") +} diff --git a/Tests/blusherTests/valid-utf8.txt b/Tests/blusherTests/valid-utf8.txt new file mode 100644 index 0000000..adfd97b --- /dev/null +++ b/Tests/blusherTests/valid-utf8.txt @@ -0,0 +1 @@ +こんにちは! From 77d8d87a22457cb5ba95eaf018a7d94f1947473f Mon Sep 17 00:00:00 2001 From: Aspen Schneider Date: Mon, 1 Jun 2026 17:17:39 +0900 Subject: [PATCH 6/6] Add `FileSystem.fileExists` method. --- .../Blusher/Core/Utilities/FileSystem.swift | 18 ++++++++++++++++++ Tests/blusherTests/blusherTests.swift | 8 ++++++++ 2 files changed, 26 insertions(+) diff --git a/Sources/Blusher/Core/Utilities/FileSystem.swift b/Sources/Blusher/Core/Utilities/FileSystem.swift index df43f75..c6aa794 100644 --- a/Sources/Blusher/Core/Utilities/FileSystem.swift +++ b/Sources/Blusher/Core/Utilities/FileSystem.swift @@ -1,3 +1,7 @@ +//================ +// C Standards +//================ + @_silgen_name("fopen") func fopen(_: UnsafePointer, _: UnsafePointer) -> OpaquePointer @@ -13,6 +17,14 @@ func fread(_: UnsafeMutableRawPointer, _: UInt, _: UInt, _: OpaquePointer) -> UI @_silgen_name("fclose") func fclose(_: OpaquePointer) -> Int32 +//=================== +// Unix Standards +//=================== + +@_silgen_name("access") +func access(_: UnsafePointer, _: Int32) -> Int32 + + public enum FileSystem { public class File { private var _filePtr: OpaquePointer? = nil @@ -62,4 +74,10 @@ public enum FileSystem { } } } + + public static func fileExists(atPath path: String) -> Bool { + path.withCString { + return access($0, 0 /* F_OK */) == 0 ? true : false + } + } } diff --git a/Tests/blusherTests/blusherTests.swift b/Tests/blusherTests/blusherTests.swift index 9905812..d994bff 100644 --- a/Tests/blusherTests/blusherTests.swift +++ b/Tests/blusherTests/blusherTests.swift @@ -20,6 +20,14 @@ import Testing } @Test func fileSystem() async throws { + // Exists. + let here = FileSystem.fileExists(atPath: "/NO.txt") + #expect(here == false) + + let there = FileSystem.fileExists(atPath: "Package.swift") + #expect(there == true) + + // Open. let file = FileSystem.File.open("Package.swift", "rb") let bytes = file.readAll() file.close()