11import MetalKit
22import CoreText
3+ import CoreGraphics
34
45// MARK: - Metal Terminal Renderer
56class MetalTerminalRenderer : NSObject {
@@ -194,9 +195,9 @@ class MetalTerminalRenderer: NSObject {
194195
195196 viewportSize = SIMD2 < Float > ( Float ( view. drawableSize. width) , Float ( view. drawableSize. height) )
196197
197- // Clear background
198+ // Clear background with Ghostty-inspired color
198199 renderPassDescriptor. colorAttachments [ 0 ] . clearColor = MTLClearColor (
199- red: 0.1 , green: 0.1 , blue: 0.1 , alpha: 1.0
200+ red: 0.086 , green: 0.086 , blue: 0.11 , alpha: 1.0
200201 )
201202
202203 // Set viewport
@@ -229,14 +230,14 @@ class MetalTerminalRenderer: NSObject {
229230 // First pass: render background colors
230231 renderEncoder. setRenderPipelineState ( backgroundPipelineState)
231232 renderEncoder. setVertexBuffer ( vertexBuffer, offset: 0 , index: 0 )
232- renderEncoder. setVertexBytes ( & viewportSize, length: MemoryLayout< SIMD2< Float>>. size, index: 0 )
233233
234234 for row in 0 ..< buffer. rows {
235235 for col in 0 ..< buffer. cols {
236236 let cell = buffer. getCell ( row: row, col: col)
237237
238238 // Skip default background
239- if cell. backgroundColor == . black { continue }
239+ let defaultBg = NSColor ( red: 0.086 , green: 0.086 , blue: 0.11 , alpha: 1.0 )
240+ if cell. backgroundColor == defaultBg { continue }
240241
241242 let rect = cellRect ( row: row, col: col)
242243 var transform = makeTransform ( rect: rect)
@@ -262,12 +263,30 @@ class MetalTerminalRenderer: NSObject {
262263
263264 guard let glyph = fontAtlas. glyph ( for: cell. character) else { continue }
264265
266+ // Create vertex buffer with proper texture coordinates for this glyph
267+ let texRect = glyph. texCoords
268+ let vertices : [ Float ] = [
269+ // Position (x, y), TexCoord (u, v)
270+ 0 , 0 , Float ( texRect. minX) , Float ( texRect. minY) , // Top-left
271+ 1 , 0 , Float ( texRect. maxX) , Float ( texRect. minY) , // Top-right
272+ 0 , 1 , Float ( texRect. minX) , Float ( texRect. maxY) , // Bottom-left
273+ 1 , 1 , Float ( texRect. maxX) , Float ( texRect. maxY) , // Bottom-right
274+ ]
275+
276+ // Create temporary buffer for this glyph
277+ guard let glyphBuffer = device. makeBuffer ( bytes: vertices,
278+ length: vertices. count * MemoryLayout< Float> . size,
279+ options: . storageModeShared) else {
280+ continue
281+ }
282+
265283 let rect = cellRect ( row: row, col: col)
266- var transform = makeTransform ( rect: rect, texCoords : glyph . texCoords )
284+ var transform = makeTransform ( rect: rect)
267285
268286 var textColor = colorToFloat4 ( cell. foregroundColor)
269287 var bgColor = colorToFloat4 ( cell. backgroundColor)
270288
289+ renderEncoder. setVertexBuffer ( glyphBuffer, offset: 0 , index: 0 )
271290 renderEncoder. setVertexBytes ( & transform, length: MemoryLayout< simd_float4x4> . size, index: 1 )
272291 renderEncoder. setFragmentBytes ( & textColor, length: MemoryLayout< SIMD4< Float>>. size, index: 0 )
273292 renderEncoder. setFragmentBytes ( & bgColor, length: MemoryLayout< SIMD4< Float>>. size, index: 1 )
@@ -279,13 +298,12 @@ class MetalTerminalRenderer: NSObject {
279298 private func renderCursor( buffer: TerminalBuffer , with renderEncoder: MTLRenderCommandEncoder ) {
280299 renderEncoder. setRenderPipelineState ( cursorPipelineState)
281300 renderEncoder. setVertexBuffer ( vertexBuffer, offset: 0 , index: 0 )
282- renderEncoder. setVertexBytes ( & viewportSize, length: MemoryLayout< SIMD2< Float>>. size, index: 0 )
283301
284302 let ( row, col) = buffer. cursorPosition
285303 let rect = cellRect ( row: row, col: col)
286304 var transform = makeTransform ( rect: rect)
287305
288- var cursorColor = SIMD4 < Float > ( 1 , 1 , 1 , 0.8 )
306+ var cursorColor = SIMD4 < Float > ( 0.5 , 0.8 , 1.0 , 0.8 ) // Nice blue cursor
289307 var time = Float ( CACurrentMediaTime ( ) )
290308
291309 renderEncoder. setVertexBytes ( & transform, length: MemoryLayout< simd_float4x4> . size, index: 1 )
@@ -297,7 +315,6 @@ class MetalTerminalRenderer: NSObject {
297315 private func renderSelection( _ selection: TerminalSelection , buffer: TerminalBuffer , with renderEncoder: MTLRenderCommandEncoder ) {
298316 renderEncoder. setRenderPipelineState ( selectionPipelineState)
299317 renderEncoder. setVertexBuffer ( vertexBuffer, offset: 0 , index: 0 )
300- renderEncoder. setVertexBytes ( & viewportSize, length: MemoryLayout< SIMD2< Float>>. size, index: 0 )
301318
302319 var selectionColor = SIMD4 < Float > ( 0.5 , 0.5 , 0.8 , 0.3 )
303320
@@ -332,9 +349,31 @@ class MetalTerminalRenderer: NSObject {
332349 }
333350
334351 private func makeTransform( rect: CGRect , texCoords: CGRect ? = nil ) -> simd_float4x4 {
335- // For now, just return identity - in real implementation this would
336- // transform the quad to the correct position
337- return simd_float4x4 ( 1 )
352+ // Create an orthographic projection matrix for 2D rendering
353+ let scaleX = 2.0 / Float( viewportSize. x)
354+ let scaleY = - 2.0 / Float( viewportSize. y) // Flip Y coordinate
355+
356+ // Translate to normalized device coordinates
357+ let translateX = - 1.0 + Float( rect. origin. x) * scaleX
358+ let translateY = 1.0 + Float( rect. origin. y) * scaleY
359+
360+ // Scale to match the cell size
361+ let sizeX = Float ( rect. width) * scaleX
362+ let sizeY = Float ( rect. height) * scaleY
363+
364+ // Build the transformation matrix
365+ var transform = simd_float4x4 ( 1 ) // Start with identity
366+
367+ // Column 0: X scaling
368+ transform. columns. 0 = SIMD4 < Float > ( sizeX, 0 , 0 , 0 )
369+ // Column 1: Y scaling
370+ transform. columns. 1 = SIMD4 < Float > ( 0 , sizeY, 0 , 0 )
371+ // Column 2: Z (unchanged)
372+ transform. columns. 2 = SIMD4 < Float > ( 0 , 0 , 1 , 0 )
373+ // Column 3: Translation
374+ transform. columns. 3 = SIMD4 < Float > ( translateX, translateY, 0 , 1 )
375+
376+ return transform
338377 }
339378
340379 private func colorToFloat4( _ color: NSColor ) -> SIMD4 < Float > {
@@ -354,6 +393,7 @@ class FontAtlas {
354393 private let device : MTLDevice
355394 private let atlasSize = 1024
356395 private var glyphs : [ Character : GlyphInfo ] = [ : ]
396+ private var atlasData : UnsafeMutableRawPointer ?
357397
358398 struct GlyphInfo {
359399 let texCoords : CGRect
@@ -369,8 +409,88 @@ class FontAtlas {
369409
370410 private func generateAtlas( ) {
371411 // Generate atlas for ASCII printable characters
372- // In a real implementation, this would create a texture atlas
373- // with all glyphs rendered using Core Text
412+ let context = CGContext (
413+ data: nil ,
414+ width: atlasSize,
415+ height: atlasSize,
416+ bitsPerComponent: 8 ,
417+ bytesPerRow: atlasSize,
418+ space: CGColorSpaceCreateDeviceGray ( ) ,
419+ bitmapInfo: CGImageAlphaInfo . none. rawValue
420+ )
421+
422+ guard let ctx = context else { return }
423+
424+ // Clear the context
425+ ctx. setFillColor ( CGColor ( gray: 0 , alpha: 1 ) )
426+ ctx. fill ( CGRect ( x: 0 , y: 0 , width: atlasSize, height: atlasSize) )
427+
428+ // Set up text rendering
429+ ctx. setFillColor ( CGColor ( gray: 1 , alpha: 1 ) )
430+ ctx. setFont ( CGFont ( font. fontName as CFString ) !)
431+ ctx. setFontSize ( font. pointSize)
432+
433+ // Render glyphs in a grid
434+ let padding : CGFloat = 2
435+ var x : CGFloat = padding
436+ var y : CGFloat = padding
437+ let lineHeight = font. capHeight + font. descender + font. leading + padding * 2
438+
439+ // ASCII printable characters (32-126)
440+ for asciiValue in 32 ... 126 {
441+ let char = Character ( UnicodeScalar ( asciiValue) !)
442+ let str = String ( char)
443+
444+ // Measure the glyph
445+ let attributes : [ NSAttributedString . Key : Any ] = [ . font: font]
446+ let size = NSAttributedString ( string: str, attributes: attributes) . size ( )
447+
448+ // Check if we need to move to the next line
449+ if x + size. width + padding > CGFloat ( atlasSize) {
450+ x = padding
451+ y += lineHeight
452+ }
453+
454+ // Skip if we're out of space
455+ if y + lineHeight > CGFloat ( atlasSize) {
456+ break
457+ }
458+
459+ // Draw the glyph
460+ ctx. saveGState ( )
461+ ctx. translateBy ( x: 0 , y: CGFloat ( atlasSize) )
462+ ctx. scaleBy ( x: 1 , y: - 1 )
463+
464+ let rect = CGRect ( x: x, y: CGFloat ( atlasSize) - y - lineHeight, width: size. width, height: lineHeight)
465+ ctx. setTextDrawingMode ( . fill)
466+
467+ let attrString = NSAttributedString ( string: str, attributes: attributes)
468+ let line = CTLineCreateWithAttributedString ( attrString)
469+ ctx. textPosition = CGPoint ( x: x, y: CGFloat ( atlasSize) - y - font. descender)
470+ CTLineDraw ( line, ctx)
471+
472+ ctx. restoreGState ( )
473+
474+ // Store glyph info
475+ let texCoords = CGRect (
476+ x: x / CGFloat( atlasSize) ,
477+ y: y / CGFloat( atlasSize) ,
478+ width: size. width / CGFloat( atlasSize) ,
479+ height: lineHeight / CGFloat( atlasSize)
480+ )
481+
482+ glyphs [ char] = GlyphInfo (
483+ texCoords: texCoords,
484+ size: size,
485+ offset: CGPoint ( x: 0 , y: 0 )
486+ )
487+
488+ // Move to next position
489+ x += size. width + padding
490+ }
491+
492+ // Store the bitmap data for texture creation
493+ self . atlasData = context? . data
374494 }
375495
376496 func createTexture( ) -> MTLTexture ? {
@@ -381,7 +501,19 @@ class FontAtlas {
381501 mipmapped: false
382502 )
383503
384- return device. makeTexture ( descriptor: descriptor)
504+ guard let texture = device. makeTexture ( descriptor: descriptor) ,
505+ let data = atlasData else {
506+ return nil
507+ }
508+
509+ texture. replace (
510+ region: MTLRegionMake2D ( 0 , 0 , atlasSize, atlasSize) ,
511+ mipmapLevel: 0 ,
512+ withBytes: data,
513+ bytesPerRow: atlasSize
514+ )
515+
516+ return texture
385517 }
386518
387519 func glyph( for character: Character ) -> GlyphInfo ? {
@@ -399,24 +531,25 @@ struct TerminalSelection {
399531
400532// MARK: - Terminal Color Palette
401533struct TerminalColorPalette {
402- let black = NSColor ( red: 0.0 , green: 0.0 , blue: 0.0 , alpha: 1.0 )
403- let red = NSColor ( red: 0.8 , green: 0.0 , blue: 0.0 , alpha: 1.0 )
404- let green = NSColor ( red: 0.0 , green: 0.8 , blue: 0.0 , alpha: 1.0 )
405- let yellow = NSColor ( red: 0.8 , green: 0.8 , blue: 0.0 , alpha: 1.0 )
406- let blue = NSColor ( red: 0.0 , green: 0.0 , blue: 0.8 , alpha: 1.0 )
407- let magenta = NSColor ( red: 0.8 , green: 0.0 , blue: 0.8 , alpha: 1.0 )
408- let cyan = NSColor ( red: 0.0 , green: 0.8 , blue: 0.8 , alpha: 1.0 )
409- let white = NSColor ( red: 0.9 , green: 0.9 , blue: 0.9 , alpha: 1.0 )
534+ // Ghostty-inspired colors
535+ let black = NSColor ( red: 0.173 , green: 0.173 , blue: 0.216 , alpha: 1 )
536+ let red = NSColor ( red: 0.937 , green: 0.325 , blue: 0.314 , alpha: 1 )
537+ let green = NSColor ( red: 0.584 , green: 0.831 , blue: 0.373 , alpha: 1 )
538+ let yellow = NSColor ( red: 0.988 , green: 0.914 , blue: 0.310 , alpha: 1 )
539+ let blue = NSColor ( red: 0.149 , green: 0.545 , blue: 0.824 , alpha: 1 )
540+ let magenta = NSColor ( red: 0.827 , green: 0.529 , blue: 0.937 , alpha: 1 )
541+ let cyan = NSColor ( red: 0.329 , green: 0.843 , blue: 0.859 , alpha: 1 )
542+ let white = NSColor ( red: 0.925 , green: 0.937 , blue: 0.953 , alpha: 1 )
410543
411- let brightBlack = NSColor ( red: 0.4 , green: 0.4 , blue: 0.4 , alpha: 1.0 )
412- let brightRed = NSColor ( red: 1.0 , green: 0.0 , blue: 0.0 , alpha: 1.0 )
413- let brightGreen = NSColor ( red: 0.0 , green: 1.0 , blue: 0.0 , alpha: 1.0 )
414- let brightYellow = NSColor ( red: 1.0 , green: 1.0 , blue: 0.0 , alpha: 1.0 )
415- let brightBlue = NSColor ( red: 0.0 , green: 0.0 , blue: 1.0 , alpha: 1.0 )
416- let brightMagenta = NSColor ( red: 1.0 , green: 0.0 , blue: 1.0 , alpha: 1.0 )
417- let brightCyan = NSColor ( red: 0.0 , green: 1.0 , blue: 1.0 , alpha: 1.0 )
418- let brightWhite = NSColor ( red: 1.0 , green: 1.0 , blue: 1.0 , alpha: 1.0 )
544+ let brightBlack = NSColor ( red: 0.373 , green: 0.373 , blue: 0.416 , alpha: 1 )
545+ let brightRed = NSColor ( red: 0.992 , green: 0.592 , blue: 0.588 , alpha: 1 )
546+ let brightGreen = NSColor ( red: 0.702 , green: 0.933 , blue: 0.612 , alpha: 1 )
547+ let brightYellow = NSColor ( red: 0.988 , green: 0.945 , blue: 0.553 , alpha: 1 )
548+ let brightBlue = NSColor ( red: 0.514 , green: 0.753 , blue: 0.988 , alpha: 1 )
549+ let brightMagenta = NSColor ( red: 0.933 , green: 0.682 , blue: 0.988 , alpha: 1 )
550+ let brightCyan = NSColor ( red: 0.596 , green: 0.929 , blue: 0.941 , alpha: 1 )
551+ let brightWhite = NSColor ( red: 0.976 , green: 0.976 , blue: 0.976 , alpha: 1 )
419552
420- let background = NSColor ( red: 0.1 , green: 0.1 , blue: 0.1 , alpha: 1.0 )
421- let foreground = NSColor ( red: 0.9 , green: 0.9 , blue: 0.9 , alpha: 1.0 )
553+ let background = NSColor ( red: 0.086 , green: 0.086 , blue: 0.11 , alpha: 1.0 )
554+ let foreground = NSColor ( red: 0.976 , green: 0.976 , blue: 0.976 , alpha: 1.0 )
422555}
0 commit comments