tiny.filigree.caret
Defined in tiny.filigree.
API (4)
Actions
Public operations.
Types and contracts
Public types and contracts.
Source
Source: lib/filigree/src/caret.zig
zig
const std = @import("std");const fixtures = @import("fixture/root.zig");const shape = @import("shape/root.zig");const unicode = @import("unicode");pub const LineHit = struct { byte_offset: usize, advance: f32,};pub fn advanceForByteOffset(run: shape.GlyphRun, byte_offset: usize, text_len: usize) f32 { const target = @min(byte_offset, text_len); var pen: f32 = 0; const map = run.clusterMap(); for (run.clusters, 0..) |cluster, cluster_index| { const start = clampedSourceOffset(cluster.source.start, text_len); const end = clampedSourceOffset(cluster.source.end, text_len); const advance = clusterAdvance(run, cluster.glyphs); if (target <= start) return pen; if (target < end) return pen + advanceWithinCluster(map, cluster_index, target, advance, text_len); pen += advance; } return pen;}pub fn hitTestAdvance(run: shape.GlyphRun, x: f32, text: []const u8) LineHit { var pen: f32 = 0; const map = run.clusterMap(); for (run.clusters, 0..) |cluster, cluster_index| { const advance = clusterAdvance(run, cluster.glyphs); if (x <= pen) { const byte_offset = clampedSourceOffset(cluster.source.start, text.len); return .{ .byte_offset = byte_offset, .advance = pen }; } if (x < pen + advance) { const stop = nearestStopInCluster(map, cluster_index, x - pen, advance, text); return .{ .byte_offset = stop.byte_offset, .advance = pen + stop.advance }; } pen += advance; } return .{ .byte_offset = text.len, .advance = pen };}pub fn hitTestAdvanceByLength(run: shape.GlyphRun, x: f32, text_len: usize) LineHit { var pen: f32 = 0; const map = run.clusterMap(); for (run.clusters, 0..) |cluster, cluster_index| { const advance = clusterAdvance(run, cluster.glyphs); if (x <= pen) { const byte_offset = clampedSourceOffset(cluster.source.start, text_len); return .{ .byte_offset = byte_offset, .advance = pen }; } if (x < pen + advance) { const stop = nearestEndpoint(map, cluster_index, x - pen, advance, text_len); return .{ .byte_offset = stop.byte_offset, .advance = pen + stop.advance }; } pen += advance; } return .{ .byte_offset = text_len, .advance = pen };}fn clampedSourceOffset(source_offset: u32, text_len: usize) usize { return @min(@as(usize, @intCast(source_offset)), text_len);}fn advanceWithinCluster(map: shape.ClusterMap, cluster_index: usize, byte_offset: usize, advance: f32, text_len: usize) f32 { const count = map.clusterCaretStopCount(cluster_index) orelse return midpointAdvance(byte_offset, cluster_index, map, advance, text_len); if (count == 0) return 0; var previous = stopInCluster(map, cluster_index, 0, advance, text_len); if (byte_offset <= previous.byte_offset) return previous.advance; var stop_index: usize = 1; while (stop_index < count) : (stop_index += 1) { const current = stopInCluster(map, cluster_index, stop_index, advance, text_len); if (byte_offset == current.byte_offset) return current.advance; if (byte_offset < current.byte_offset) { const span = current.byte_offset - previous.byte_offset; if (span == 0) return current.advance; const numerator: f32 = @floatFromInt(byte_offset - previous.byte_offset); const denominator: f32 = @floatFromInt(span); return previous.advance + (current.advance - previous.advance) * (numerator / denominator); } previous = current; } return previous.advance;}fn midpointAdvance(byte_offset: usize, cluster_index: usize, map: shape.ClusterMap, advance: f32, text_len: usize) f32 { const range = map.clusterSourceRange(cluster_index) orelse return 0; const start = clampedSourceOffset(range.start, text_len); const end = clampedSourceOffset(range.end, text_len); if (end <= start) return 0; if (byte_offset <= start) return 0; if (byte_offset >= end) return advance; const numerator: f32 = @floatFromInt(byte_offset - start); const denominator: f32 = @floatFromInt(end - start); return advance * (numerator / denominator);}fn nearestStopInCluster(map: shape.ClusterMap, cluster_index: usize, x: f32, advance: f32, text: []const u8) LineHit { const count = map.clusterCaretStopCount(cluster_index) orelse 0; if (count > 2) return nearestExplicitStopInCluster(map, cluster_index, x, advance, text.len); const range = map.clusterSourceRange(cluster_index) orelse return .{ .byte_offset = text.len, .advance = advance }; const start = clampedSourceOffset(range.start, text.len); const end = clampedSourceOffset(range.end, text.len); const fraction = if (advance <= 0) @as(f32, 0) else std.math.clamp(x / advance, @as(f32, 0), @as(f32, 1)); const byte_offset = byteOffsetInGraphemeSegments(text, start, end, fraction); return .{ .byte_offset = byte_offset, .advance = advanceWithinCluster(map, cluster_index, byte_offset, advance, text.len), };}fn nearestExplicitStopInCluster(map: shape.ClusterMap, cluster_index: usize, x: f32, advance: f32, text_len: usize) LineHit { const count = map.clusterCaretStopCount(cluster_index) orelse 0; var nearest = stopInCluster(map, cluster_index, 0, advance, text_len); var nearest_distance = @abs(x - nearest.advance); var stop_index: usize = 1; while (stop_index < count) : (stop_index += 1) { const stop = stopInCluster(map, cluster_index, stop_index, advance, text_len); const distance = @abs(x - stop.advance); if (distance < nearest_distance or (distance == nearest_distance and stop.byte_offset > nearest.byte_offset)) { nearest = stop; nearest_distance = distance; } } return nearest;}fn nearestEndpoint(map: shape.ClusterMap, cluster_index: usize, x: f32, advance: f32, text_len: usize) LineHit { const range = map.clusterSourceRange(cluster_index) orelse return .{ .byte_offset = text_len, .advance = advance }; const start = clampedSourceOffset(range.start, text_len); const end = clampedSourceOffset(range.end, text_len); if (x < advance * 0.5) return .{ .byte_offset = start, .advance = 0 }; return .{ .byte_offset = end, .advance = advance };}fn stopInCluster(map: shape.ClusterMap, cluster_index: usize, stop_index: usize, advance: f32, text_len: usize) LineHit { const count = map.clusterCaretStopCount(cluster_index) orelse 0; const stop = map.clusterCaretStop(cluster_index, stop_index) orelse 0; if (stop_index == 0) { return .{ .byte_offset = clampedSourceOffset(stop, text_len), .advance = 0 }; } if (stop_index + 1 >= count) { return .{ .byte_offset = clampedSourceOffset(stop, text_len), .advance = advance }; } if (map.clusterLigatureCaret(cluster_index, stop_index - 1)) |caret| { return .{ .byte_offset = clampedSourceOffset(stop, text_len), .advance = @as(f32, @floatFromInt(caret.x_offset)) / 64.0, }; } const range = map.clusterSourceRange(cluster_index) orelse return .{ .byte_offset = clampedSourceOffset(stop, text_len), .advance = advance }; const start = clampedSourceOffset(range.start, text_len); const end = clampedSourceOffset(range.end, text_len); if (end <= start) return .{ .byte_offset = clampedSourceOffset(stop, text_len), .advance = 0 }; const relative = clampedSourceOffset(stop, text_len) - start; const span = end - start; return .{ .byte_offset = clampedSourceOffset(stop, text_len), .advance = advance * (@as(f32, @floatFromInt(relative)) / @as(f32, @floatFromInt(span))), };}fn byteOffsetInGraphemeSegments(text: []const u8, start: usize, end: usize, fraction: f32) usize { if (end <= start) return start; const safe_start = @min(start, text.len); const safe_end = @min(@max(end, safe_start), text.len); if (safe_end <= safe_start) return safe_start; const segment_count = graphemeSegmentCount(text, safe_start, safe_end) catch return nearestEndpointByFraction(safe_start, safe_end, fraction); if (segment_count == 0) return safe_start; const target = @min( @as(usize, @intFromFloat(@round(std.math.clamp(fraction, @as(f32, 0), @as(f32, 1)) * @as(f32, @floatFromInt(segment_count))))), segment_count, ); if (target == 0) return safe_start; if (target >= segment_count) return safe_end; return graphemeBoundaryAt(text, safe_start, safe_end, target) catch nearestEndpointByFraction(safe_start, safe_end, fraction);}fn graphemeSegmentCount(text: []const u8, start: usize, end: usize) !usize { var iterator = try unicode.SourceIterator.init(.{ .utf8 = text[start..end] }, @intCast(start)); var state: unicode.GraphemeState = .{}; var count: usize = 0; while (try iterator.next()) |scalar| { if (!state.consume(scalar.codepoint)) count += 1; } return count;}fn graphemeBoundaryAt(text: []const u8, start: usize, end: usize, target: usize) !usize { var iterator = try unicode.SourceIterator.init(.{ .utf8 = text[start..end] }, @intCast(start)); var state: unicode.GraphemeState = .{}; var index: usize = 0; while (try iterator.next()) |scalar| { if (!state.consume(scalar.codepoint)) { if (index == target) return @intCast(scalar.source.start); index += 1; } } return end;}fn nearestEndpointByFraction(start: usize, end: usize, fraction: f32) usize { if (fraction < 0.5) return start; return end;}fn clusterAdvance(run: shape.GlyphRun, span: shape.GlyphSpan) f32 { var advance: f32 = 0; var index: usize = @intCast(span.start); const end: usize = @min(@as(usize, @intCast(span.end)), run.glyphs.len); while (index < end) : (index += 1) { advance += @as(f32, @floatFromInt(run.glyphs[index].x_advance)) / 64.0; } return advance;}test "caret hit test chooses grapheme boundaries inside clusters" { const glyphs = [_]shape.ShapedGlyph{ .{ .glyph_id = 1, .cluster = 0, .cluster_index = 0, .source_start = 0, .source_end = 2, .source_codepoint_count = 2, .x_advance = 1152, .y_advance = 0, .x_offset = 0, .y_offset = 0 }, .{ .glyph_id = 2, .cluster = 2, .cluster_index = 1, .source_start = 2, .source_end = 3, .x_advance = 576, .y_advance = 0, .x_offset = 0, .y_offset = 0 }, }; const clusters = [_]shape.Cluster{ .{ .source = .{ .start = 0, .end = 2 }, .glyphs = .{ .start = 0, .end = 1 }, .codepoint_count = 2 }, .{ .source = .{ .start = 2, .end = 3 }, .glyphs = .{ .start = 1, .end = 2 } }, }; const run = shape.GlyphRun{ .glyphs = &glyphs, .clusters = &clusters, .ligature_carets = &.{}, .total_x_advance = 1728, .total_y_advance = 0, .direction = .ltr, .writing_mode = .horizontal, .output_order = .visual, }; try std.testing.expectEqual(@as(f32, 0), advanceForByteOffset(run, 0, 3)); try std.testing.expectEqual(@as(f32, 9), advanceForByteOffset(run, 1, 3)); try std.testing.expectEqual(@as(f32, 18), advanceForByteOffset(run, 2, 3)); try std.testing.expectEqual(@as(f32, 27), advanceForByteOffset(run, 3, 3)); try std.testing.expectEqual(LineHit{ .byte_offset = 0, .advance = 0 }, hitTestAdvance(run, 4.4, "=>x")); try std.testing.expectEqual(LineHit{ .byte_offset = 1, .advance = 9 }, hitTestAdvance(run, 9, "=>x")); try std.testing.expectEqual(LineHit{ .byte_offset = 2, .advance = 18 }, hitTestAdvance(run, 16, "=>x")); try std.testing.expectEqual(LineHit{ .byte_offset = 3, .advance = 27 }, hitTestAdvance(run, 24, "=>x"));}test "caret hit test uses ligature stops" { const glyphs = [_]shape.ShapedGlyph{ .{ .glyph_id = 9, .cluster = 0, .cluster_index = 0, .source_start = 0, .source_end = 2, .source_codepoint_count = 2, .x_advance = 640, .y_advance = 0, .x_offset = 0, .y_offset = 0, .glyph_class = .ligature, .ligature_caret_start = 0, .ligature_caret_count = 1, }, }; const clusters = [_]shape.Cluster{ .{ .source = .{ .start = 0, .end = 2 }, .glyphs = .{ .start = 0, .end = 1 }, .codepoint_count = 2 }, }; const carets = [_]shape.LigatureCaret{ .{ .x_offset = 384 }, }; const run = shape.GlyphRun{ .glyphs = &glyphs, .clusters = &clusters, .ligature_carets = &carets, .total_x_advance = 640, .total_y_advance = 0, .direction = .ltr, .writing_mode = .horizontal, .output_order = .visual, }; try std.testing.expectEqual(@as(f32, 6), advanceForByteOffset(run, 1, 2)); try std.testing.expectEqual(LineHit{ .byte_offset = 1, .advance = 6 }, hitTestAdvance(run, 5.1, "fi")); try std.testing.expectEqual(LineHit{ .byte_offset = 2, .advance = 10 }, hitTestAdvance(run, 8.3, "fi"));}test "caret hit test uses OpenType GDEF ligature stops" { const text = "fi"; const bytes = try fixtures.createWithGsubLigatureAndGdefCarets(std.testing.allocator); defer std.testing.allocator.free(bytes); var shaper = shape.Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult; defer shaper.deinit(); shaper.setPixelHeightScale(20); var context = shape.Context.init(std.testing.allocator, .{}); defer context.deinit(); var output = try shape.Output.init(std.testing.allocator, .{ .max_glyphs = 256, .max_ligature_carets = 256, }); defer output.deinit(std.testing.allocator); try context.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = text } }, &output); const run = output.run(); const map = run.clusterMap(); const caret = map.clusterLigatureCaret(0, 0).?; try std.testing.expectEqual(@as(usize, 1), run.glyphs.len); try std.testing.expectEqual(@as(usize, 1), run.ligature_carets.len); try std.testing.expectEqual(false, caret.synthesized); try std.testing.expectEqual(@as(f32, 5), advanceForByteOffset(run, 1, text.len)); try std.testing.expectEqual(LineHit{ .byte_offset = 1, .advance = 5 }, hitTestAdvance(run, 4.6, text)); try std.testing.expectEqual(LineHit{ .byte_offset = 2, .advance = 10 }, hitTestAdvance(run, 7.6, text));}Source: lib/filigree/src/root.zig:10
zig
pub const caret = @import("caret.zig");Audit
| Definitions | 5 |
|---|---|
| Public names | 5 |
| Members | 2 |
| Version | 26.7.0 |
| Revision | daab053ee433 |