My first idea was based on Text + operator. Seems to be easy, constructing the whole Text by composition /one by one character/ and check the width of partial result ... Unfortunately, I didn't find the way how to do it. All the tricks known to get some geometry (alignmentGuide, GeometryReader, anchorPreferences ...) works as View modifiers! This means the Text + operator is unusable. Simply calculate the position of characters in Text as a sum of Text(String(Character)) widths doesn't work, for example
Text("WAW")
and
HStack(spacing:0) { Text("W"); Text("A"); Text("W") }
width is (as expected) different.
Finally I got (use copy paste to check it) something like
struct ContentView: View {
    @State var width: [CGFloat] = []
    let font = Font.system(size: 100)
    var body: some View {
        VStack {
            if width.isEmpty {
                text(t: Text("W").font(font), width: $width)
                text(t: Text("WA").font(font), width: $width)
                text(t: Text("WAW").font(font), width: $width)
                text(t: Text("WAWE").font(font), width: $width)
            } else {
                ZStack(alignment: .topLeading) {
                    Text("WAWE").font(font).border(Color.red)
                    Path { path in
                        path.move(to: CGPoint(x: 0, y: 0))
                        path.addLine(to: CGPoint(x: 0, y: 150))
                    }.stroke(lineWidth: 1)
                    Text("\(0)").rotationEffect(Angle(degrees: 90), anchor: .bottom)
                        .position(CGPoint(x: 0, y: 170))
                    ForEach(Array(width.sorted(by: <).enumerated()), id: \.0) { p in
                        ZStack {
                            Path { path in
                                path.move(to: CGPoint(x: p.1, y: 0))
                                path.addLine(to: CGPoint(x: p.1, y: 150))
                            }.stroke(lineWidth: 1)
                            Text("\(p.1)").rotationEffect(Angle(degrees: 90), anchor: .bottom).position(CGPoint(x: p.1, y: 170))
                        }
                    }
                }.padding()
            }
        }
    }
}
func text(t: Text, width: Binding<[CGFloat]>)->some View {
    let tt = t.background(
        GeometryReader{ proxy->Color in
            DispatchQueue.main.async {
                width.wrappedValue.append(proxy.size.width)
            }
            return Color.clear
        }
    )
    return tt.background(Color.yellow)
}
with this result
Which works but is very hacking solution
I am looking for the better way!



