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 transcript Elixir 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