defmodule Features.RichTranscript.View do
use GPUI.View
alias GPUI.Text.{Edit, Position, Range, RichRun, Selection, Transaction}
alias GPUI.UI
@impl GPUI.View
def render(assigns) do
~GPUI"""
Neutral rich transcriptElixir owns content and runs; native owns shaping, selection, and measurement
{Enum.map(assigns.messages, &message/1)}
Enter submits · Shift+Enter adds a line · draft revision {assigns.draft_revision}{assigns.draft_status}
{assigns.visible}{assigns.status}
"""
end
@impl GPUI.View
def handle_event("draft-transaction", %{revision: revision}, assigns),
do: {:noreply, %{assigns | draft_revision: revision, draft_status: "Draft updated"}}
def handle_event("submit-draft", %{value: value}, assigns) do
case String.trim(value) do
"" ->
{:noreply, %{assigns | draft_status: "Write something before submitting"}}
text ->
number = next_number(assigns.messages)
{:ok, revision} = clear_draft(assigns.draft_buffer)
message =
message_data(number)
|> Map.put(:body, text <> "\nOpen details")
{:noreply,
%{
assigns
| messages: assigns.messages ++ [message],
follow_request: assigns.follow_request + 1,
draft_revision: revision,
draft_status: "Submitted message #{number}",
composer_focus_request: assigns.composer_focus_request + 1
}}
end
end
def handle_event("visible-range", %{value: range}, assigns),
do: {:noreply, %{assigns | visible: "visible #{range.first}–#{range.last}"}}
def handle_event("open-link", %{type: :link, value: link}, assigns),
do: {:noreply, %{assigns | status: "Activated #{link}"}}
def handle_event("append", _event, assigns) do
number = next_number(assigns.messages)
{:noreply, %{assigns | messages: assigns.messages ++ [message_data(number)]}}
end
def handle_event("stream", _event, assigns) do
id = List.last(assigns.messages).id
messages =
Enum.map(assigns.messages, fn
%{id: ^id} = message ->
%{
message
| body: message.body <> "\nA streamed paragraph changed this item's measured height.",
revision: message.revision + 1
}
message ->
message
end)
{:noreply, %{assigns | messages: messages}}
end
def handle_event("prepend", _event, assigns) do
first = hd(assigns.messages).number
older = Enum.map((first - 3)..(first - 1), &message_data/1)
{:noreply, %{assigns | messages: older ++ assigns.messages}}
end
defp next_number(messages), do: List.last(messages).number + 1
defp clear_draft(buffer) do
{:ok, snapshot} = GPUI.Text.Buffer.snapshot(buffer)
start_position = Position.new(0, 0)
end_position = end_position(snapshot.text)
{:ok, %{revision: revision}} =
GPUI.Text.Buffer.transact(buffer, %Transaction{
id: "transcript-submit-#{System.unique_integer([:positive])}",
base_revision: snapshot.revision,
edits: [Edit.new(Range.new(start_position, end_position), "")],
selections: [Selection.caret("primary", start_position, primary: true)]
})
{:ok, revision}
end
defp end_position(text) do
lines = String.split(text, "\n", trim: false)
Position.new(Enum.count(lines) - 1, lines |> List.last() |> utf16_length())
end
def message_data(number) do
body =
if rem(number, 3) == 0 do
"Message #{number} has a longer body that wraps naturally. Native shaping determines its height while the collection retains the scroll anchor.\nOpen details"
else
"Message #{number} demonstrates neutral rich text.\nOpen details"
end
%{id: "message-#{number}", number: number, body: body, revision: 0}
end
defp message(message) do
details = "Open details"
{details_start, _length} = :binary.match(message.body, details)
{line, column} = byte_position(message.body, details_start)
runs = [
RichRun.new(Range.new(Position.new(0, 0), Position.new(0, 9)),
font_weight: :semibold,
color: 0xF8FAFC
),
RichRun.new(
Range.new(
Position.new(line, column),
Position.new(line, column + String.length(details))
),
color: 0x60A5FA,
underline: 0x60A5FA,
link: "message://#{message.id}"
)
]
assigns = Map.put(message, :runs, runs)
~GPUI"""
"""
end
defp byte_position(text, byte_offset) do
prefix = binary_part(text, 0, byte_offset)
lines = String.split(prefix, "\n", trim: false)
{length(lines) - 1, lines |> List.last() |> utf16_length()}
end
defp utf16_length(text) do
text
|> :unicode.characters_to_binary(:utf8, {:utf16, :little})
|> byte_size()
|> Kernel.div(2)
end
end
defmodule Features.RichTranscript.App do
use GPUI.Application
@impl GPUI.Application
def mount(_args) do
messages = Enum.map(1..18, &Features.RichTranscript.View.message_data/1)
{:ok, draft_buffer} = GPUI.Text.Buffer.new("")
{:ok,
[
window "Rich Transcript" do
size(760, 720)
root(Features.RichTranscript.View,
messages: messages,
draft_buffer: draft_buffer,
draft_revision: 0,
draft_status: "Ready",
composer_focus_request: 1,
follow_request: 0,
visible: "range pending",
status: "Select and copy text, or activate a link"
)
end
]}
end
end