class PresenterView < SlideView

Inherits from
SlideView: #render_slide_path, #render_slide!

The presenter-facing view with notes, timing, and slide previews.

Shows the current slide, next slide preview, presenter notes, timing controls, and pacing indicators. Updates the timing display every second via a background task.

Definitions

def initialize(id, data, controller:)

Initialize a new presenter view.

Signature

parameter id String

The unique element identifier.

parameter data Hash

The element data attributes.

parameter controller PresentationController

The shared presentation controller.

Implementation

def initialize(id, data, controller:)
	super(id, data)
	@controller = controller
	@clock_task = nil
	@timing_state = nil
	@preview_renderer = SlideRenderer.new(css_class: "slide preview-slide", templates: controller.templates)
end

def bind(page)

Bind this view to a page and start the timing update loop.

Signature

parameter page Live::Page

The page this view is bound to.

Implementation

def bind(page)
	super
	@controller.add_listener(self)
	
	# Update only the timing section every second.
	@clock_task = Async do
		while true
			update_timing!
			sleep 1
		end
	end
end

def close

Close this view and stop the timing update loop.

Implementation

def close
	@clock_task&.stop
	@controller.remove_listener(self)
	super
end

def slide_changed!

Called by the controller when the slide changes.

Implementation

def slide_changed!
	self.render_slide!
end

def update_timing!

Update the timing section and the Next tooltip when the clock state changes. Leave unchanged, paused controls in place to preserve keyboard focus.

Implementation

def update_timing!
	clock = @controller.clock
	state_changed = @timing_state != [clock.started?, clock.running?]
	return unless clock.running? || state_changed
	
	replace(".timing") do |builder|
		render_timing(builder, @controller.current_slide)
	end
	
	if state_changed
		replace(".next-button") do |builder|
			render_next_button(builder)
		end
	end
end

def handle(event)

Handle an event from the client.

Signature

parameter event Hash

The event data with :detail containing the action.

Implementation

def handle(event)
	action = event.dig(:detail, :action)
	
	case action
	when "next"
		@controller.advance!
	when "previous"
		@controller.retreat!
	when "pause"
		if !@controller.clock.started?
			@controller.clock.start!
		elsif @controller.clock.paused?
			@controller.clock.resume!
		else
			@controller.clock.pause!
		end
		@controller.save_state!
		update_timing! if @page
	when "reset"
		@controller.reset_timer!
	when "reload"
		@controller.reload!
	when "jump"
		if index = event.dig(:detail, :index)
			@controller.go_to(index.to_i)
		end
	end
end

def editor_url_for(path, line = 1)

Generate an editor URL for the given file path.

Signature

parameter path String

The file path.

parameter line Integer

The line number.

returns String | Nil

The editor URL, or nil.

Implementation

def editor_url_for(path, line = 1)
	Editor.url_for(path, line)
end

def timer_action_hint

Describe the actual effect of the outgoing slide's timer metadata.

Signature

returns String | Nil

The hint, or nil when there is no timer action or next slide.

Implementation

def timer_action_hint
	return unless @controller.next_slide
	
	clock = @controller.clock
	case @controller.current_slide.timer
	when "start"
		return "Advancing will start the timer." unless clock.started?
	when "pause"
		return "Advancing will pause the timer." if clock.running?
	when "resume"
		return "Advancing will resume the timer." if clock.paused?
	else
		return
	end
	
	if clock.running?
		"Advancing will leave the timer running."
	elsif clock.paused?
		"Timer is paused. Advancing will leave it paused."
	else
		"Advancing will leave the timer stopped."
	end
end

def render_next_button(builder)

Render the Next button with a tooltip describing its timer action.

Signature

parameter builder XRB::Builder

The HTML builder.

Implementation

def render_next_button(builder)
	builder.tag(:button,
		class: "next-button",
		title: timer_action_hint,
		onClick: forward_event(action: "next")
	) do
		builder.text("Next →")
	end
end

def render_timing(builder, slide)

Render the timing bar with controls, elapsed/remaining time, and pacing.

Signature

parameter builder XRB::Builder

The HTML builder.

parameter slide Slide | Nil

The current slide.

Implementation

def render_timing(builder, slide)
	@timing_state = [@controller.clock.started?, @controller.clock.running?]
	pacing = @controller.pacing
	progress = pacing ? (@controller.slide_progress * 100).round(1) : 0.0
	next_slide = @controller.next_slide
	wait_for_advance = !@controller.clock.started? && slide&.timer == "start" && !!next_slide
	builder.tag(:div, class: "timing", style: "--slide-progress: #{progress}%") do
		pacing_class = case pacing
		when :behind then "behind"
		when :ahead then "ahead"
		when :on_time then "on-time"
		end
		
		builder.tag(:div, class: "toolbar timing-info #{pacing_class}") do
			if wait_for_advance
				builder.tag(:span,
					class: "auto-start",
					role: "status",
					title: "Advancing will start the timer."
				) do
					builder.tag(:span, class: "auto-start-icon", "aria-hidden": "true"){builder.text("▶")}
					builder.text("Auto-start")
				end
			else
				builder.tag(:button,
					class: "pause-button",
					onClick: forward_event(action: "pause")
				) do
					label = if !@controller.clock.started?
						"▶ Start"
					elsif @controller.clock.paused?
						"▶ Resume"
					else
						"⏸ Pause"
					end
					builder.text(label)
				end
			end
			
			if @controller.clock.paused? && slide
				title = if slide.timer == "start"
					"Reset to this slide's timestamp and wait to start"
				else
					"Reset to this slide's timestamp and stay paused"
				end
				
				builder.tag(:button,
					class: "pause-button",
					title: title,
					onClick: forward_event(action: "reset")
				) do
					builder.text("↺ Reset")
				end
			end
			
			builder.tag(:span, class: "elapsed") do
				builder.text("Elapsed: #{format_duration(@controller.clock.elapsed)}")
			end
			
			if pacing
				builder.tag(:span, class: "remaining") do
					builder.text("Remaining: #{format_duration(@controller.time_remaining)}")
				end
				
				builder.tag(:span, class: "pacing-indicator") do
					indicator = case pacing
					when :behind then "⏩ Speed up"
					when :ahead then "⏪ Slow down"
					else "✓ On time"
					end
					builder.text(indicator)
				end
			end
			
			if slide
				builder.tag(:span, class: "slide-duration") do
					builder.text("Duration: #{format_duration(slide.duration)}")
				end
			end
			
			# Speaker display — only shown when front matter includes a `speaker` key.
			if (current_speaker = slide&.speaker)
				builder.tag(:span, class: "speaker-info") do
					builder.tag(:span, class: "speaker-label"){builder.text("🎤")}
					builder.text(" #{current_speaker}")
					
					# Show upcoming speaker only when they differ from the current one.
					if (next_speaker = next_slide&.speaker) && next_speaker != current_speaker
						builder.tag(:span, class: "next-speaker") do
							builder.text(" → #{next_speaker}")
						end
					end
				end
			end
		end
	end
end

def format_duration(seconds)

Format a duration in seconds as M:SS.

Signature

parameter seconds Numeric

The duration in seconds.

returns String

The formatted duration string.

Implementation

def format_duration(seconds)
	seconds = seconds.to_i
	minutes = seconds / 60
	secs = seconds % 60
	format("%d:%02d", minutes, secs)
end

def render(builder)

Render the full presenter view.

Signature

parameter builder XRB::Builder

The HTML builder.

Implementation

def render(builder)
	slide = @controller.current_slide
	next_slide = @controller.next_slide
	
	builder.tag(:div, class: "presenter") do
		# Controls bar
		builder.tag(:div, class: "toolbar controls") do
			builder.tag(:button,
				onClick: forward_event(action: "previous")
			) do
				builder.text("← Previous")
			end
			
			render_next_button(builder)
			
			builder.tag(:span, class: "slide-info") do
				builder.tag(:span, class: "slide-position") do
					builder.text("Slide #{@controller.current_index + 1} of #{@controller.slide_count}")
				end
				
				if slide
					render_slide_path(builder, slide.path)
					
					if editor_url = editor_url_for(slide.source_path)
						builder.tag(:a, href: editor_url, class: "edit-link") do
							builder.text("✎")
						end
					end
				end
			end
			
			# Jump-to dropdown for marked slides
			markers = []
			@controller.slides.each_with_index do |s, i|
				if s.marker
					markers << [i, s.marker]
				end
			end
			
			unless markers.empty?
				builder.tag(:select,
					class: "jump-to",
					"data-live-id": @id
				) do
					builder.tag(:option, value: "", disabled: true, selected: true) do
						builder.text("Jump to…")
					end
					
					markers.each do |index, label|
						builder.tag(:option, value: index) do
							builder.text(label)
						end
					end
				end
			end
			
			builder.tag(:button,
				onClick: forward_event(action: "reload"),
				class: "reload"
			) do
				builder.text("↻ Reload")
			end
		end
		
		# Slide previews
		builder.tag(:div, class: "previews") do
			# Current slide
			builder.tag(:div, class: "preview current-preview") do
				builder.tag(:h3){builder.text("Current")}
				builder.tag(:div, class: "preview-frame slide-viewport") do
					@preview_renderer.render(builder, slide)
				end
			end
			
			# Next slide
			builder.tag(:div, class: "preview next-preview") do
				builder.tag(:h3){builder.text("Next")}
				builder.tag(:div, class: "preview-frame slide-viewport") do
					if next_slide
						@preview_renderer.render(builder, next_slide)
					else
						builder.tag(:div, class: "no-slide") do
							builder.text("End of presentation")
						end
					end
				end
			end
		end
		
		# Timing
		render_timing(builder, slide)
		
		# Presenter notes
		builder.tag(:div, class: "notes") do
			builder.tag(:h3){builder.text("Notes")}
			builder.tag(:div, class: "notes-content") do
				if notes = slide&.notes
					builder.raw(notes.to_html)
				else
					builder.tag(:p, class: "no-notes"){builder.text("No presenter notes for this slide.")}
				end
			end
		end
	end
end