use concinnity_core::components::FrameInput;
use concinnity_core::ecs::World;
use concinnity_core::render::shader_programs::vocabulary::ENTRIES;
use super::shaders_state::{SourceState, vocabulary_of};
use crate::debug::hot_reload::ReloadSubject;
use crate::editor::hook::EditorHook;
use crate::editor::notify;
use crate::editor::panels::registry::PanelKey;
use crate::editor::panels::sdf_field_list;
use crate::editor::panels::shader_diagnostics::{Status, Tone};
use crate::editor::panels::shader_reference::{self, RefClick, RefRow};
use crate::editor::panels::shader_reference_panel::{self, RefView};
use crate::editor::panels::shader_source::{self, Leave, SourceKey};
use crate::editor::panels::shader_source_panel::{self, SourceAction, SourceView};
use crate::editor::text_area::clipboard;
use crate::editor::text_area::keys::Platform;
use crate::editor::text_area::layout::{self, Geometry, Metrics, TextAreaView};
impl EditorHook {
pub(in crate::editor::hook) fn open_shader_file(&mut self, key: SourceKey) {
self.leave_shader_source(Leave::Open(key));
}
pub(in crate::editor::hook) fn close_shader_source(&mut self) {
self.leave_shader_source(Leave::Close);
}
pub(in crate::editor::hook) fn leave_shader_source(&mut self, then: Leave) {
if let Some(src) = &self.shaders.source
&& shader_source::must_ask(src.area.is_dirty(), &src.key, &then)
{
let prompt = shader_source::leave_prompt(src.file_name());
self.open_modal(&prompt, shader_source::leave_buttons(then));
return;
}
self.go_to_leave(then);
}
pub(in crate::editor::hook) fn answer_leave_shader_source(
&mut self,
save: bool,
then: Leave,
world: &mut World,
) {
if save && !self.save_shader_source() {
return;
}
let confirm = then == Leave::ConfirmForm;
self.go_to_leave(then);
if confirm {
self.confirm_form(world);
}
}
fn go_to_leave(&mut self, then: Leave) {
match then {
Leave::Close | Leave::ConfirmForm => self.shaders.source = None,
Leave::Open(key) => self.load_shader_source(key),
Leave::Edit(edit) => {
self.shaders.source = None;
self.apply_shader_edit(edit);
}
}
}
fn load_shader_source(&mut self, key: SourceKey) {
if self.shaders.source.as_ref().is_some_and(|s| s.key == key) {
self.focus_panel(PanelKey::ShaderSource);
return;
}
let path = match &key {
SourceKey::Shader { name, stage } => self
.declared_shaders()
.into_iter()
.find(|s| &s.name == name)
.and_then(|s| s.file(*stage).map(|f| f.path.clone())),
SourceKey::Field { path } => self
.declared_fields()
.into_iter()
.find(|f| &f.path == path)
.map(|f| f.path),
};
let Some(path) = path else {
return;
};
let (text, status) = match std::fs::read_to_string(&path) {
Ok(text) => (text, None),
Err(e) => (
String::new(),
Some(Status::new(format!("{path}: {e}"), Tone::Error)),
),
};
let mut src = SourceState::new(key, path, text);
src.status = status;
src.focus = true;
self.shaders.source = Some(src);
self.focus_panel(PanelKey::ShaderSource);
}
pub(in crate::editor::hook) fn save_shader_source(&mut self) -> bool {
let Some(src) = self.shaders.source.as_mut() else {
return false;
};
let text = src.area.text();
if let Err(e) = std::fs::write(&src.path, &text) {
let message = format!("{}: {e}", src.path);
src.status = Some(Status::new(message.clone(), Tone::Error));
self.notifier.error_with(
&format!("Save failed: {message}"),
notify::Action::OpenConsole,
);
return false;
}
src.saved(text);
if src.live == Some(false) {
self.require_rebuild();
}
true
}
fn source_subjects(&mut self) -> Vec<ReloadSubject> {
match self.shaders.source.as_ref().map(|s| s.key.clone()) {
Some(SourceKey::Shader { name, .. }) => vec![ReloadSubject::shader(&name)],
Some(SourceKey::Field { path }) => self
.declared_fields()
.iter()
.find(|f| f.path == path)
.map(sdf_field_list::subjects)
.unwrap_or_default(),
None => Vec::new(),
}
}
pub(in crate::editor::hook) fn drive_shader_source(&mut self) {
let now = self.clock.elapsed().as_secs_f64();
if self.shaders.source.is_none() {
return;
}
let subjects = self.source_subjects();
let Some(src) = self.shaders.source.as_mut() else {
return;
};
if src.take_board(&self.shaders.reports.snapshot(), &subjects)
&& let Some((line, column)) = src.jump
{
src.area.go_to(line, column);
}
src.check_disk(now);
}
pub(in crate::editor::hook) fn shader_typing(&self) -> bool {
self.shaders.source.as_ref().is_some_and(|s| s.focus)
&& self.panel_order.last() == Some(&PanelKey::ShaderSource)
}
fn shader_geometry(&mut self) -> Option<Geometry> {
let vp = self.viewport;
let o = self.origin(PanelKey::ShaderSource, vp);
let s = self.effective_size(PanelKey::ShaderSource);
let reference = self.shaders.reference.open;
let src = self.shaders.source.as_mut()?;
let g = shader_source_panel::area_geometry(o, s, reference, &src.area, Metrics::code());
src.area.set_view(g.view());
Some(g)
}
pub(in crate::editor::hook) fn scroll_shader_source(&mut self, delta: f32, mx: f32, my: f32) {
let vp = self.viewport;
let o = self.origin(PanelKey::ShaderSource, vp);
let s = self.effective_size(PanelKey::ShaderSource);
if self.shaders.reference.open && shader_source_panel::cursor_over_reference(mx, my, o, s) {
let step = if delta > 0.0 { -1 } else { 1 };
self.scroll_reference(step);
return;
}
self.shader_geometry();
let shift = self.shift_held;
if let Some(src) = self.shaders.source.as_mut() {
src.area.wheel(delta, shift);
}
}
fn shader_status(src: &SourceState) -> Option<Status> {
src.status.clone().or_else(|| {
(src.live == Some(false)).then(|| {
Status::new(
"Not in the running world yet; it joins when the world rebuilds",
Tone::Info,
)
})
})
}
fn reference_rows(&self) -> Vec<RefRow<'static>> {
let entries = self
.shaders
.source
.as_ref()
.map_or(ENTRIES, |s| vocabulary_of(&s.key));
self.shaders.reference.rows(entries)
}
fn scroll_reference(&mut self, step: isize) {
let vp = self.viewport;
let o = self.origin(PanelKey::ShaderSource, vp);
let s = self.effective_size(PanelKey::ShaderSource);
let shown = shader_reference_panel::rows_shown(shader_source_panel::reference_rect(o, s));
let total = self.reference_rows().len();
self.shaders.reference.scroll_by(step, total, shown);
}
pub(in crate::editor::hook) fn make_source_view<'a>(
&'a self,
src: &'a SourceState,
title: &'a str,
status: Option<&'a Status>,
rows: &'a [RefRow<'a>],
mouse: [f32; 2],
) -> SourceView<'a> {
let reference = &self.shaders.reference;
SourceView {
title,
path: &src.path,
area: &src.area,
focus: self.shader_typing(),
status,
markers: &src.markers,
mouse,
reference: reference.open.then_some(RefView {
rows,
scroll: reference.scroll,
mouse,
}),
}
}
pub(in crate::editor::hook) fn draw_shader_source(
&self,
world: &mut World,
o: [f32; 2],
mouse: [f32; 2],
) {
let Some(src) = &self.shaders.source else {
shader_source_panel::ids().hide(world);
return;
};
let s = self.effective_size(PanelKey::ShaderSource);
let title = src.title();
let status = Self::shader_status(src);
let rows = self.reference_rows();
let view = self.make_source_view(src, &title, status.as_ref(), &rows, mouse);
shader_source_panel::place(world, Some(&view), o, s, Metrics::code());
}
pub(in crate::editor::hook) fn apply_source_action(
&mut self,
action: SourceAction,
mx: f32,
my: f32,
) {
match action {
SourceAction::Text => {
let Some(g) = self.shader_geometry() else {
return;
};
let now = self.clock.elapsed().as_secs_f64();
let shift = self.shift_held;
let Some(src) = self.shaders.source.as_mut() else {
return;
};
let marker = {
let view = TextAreaView {
area: &src.area,
focused: true,
markers: &src.markers,
mouse: [mx, my],
};
layout::hovered_marker(&view, &g).map(|m| (m.line, m.column))
};
match marker {
Some((line, column)) => src.area.go_to(line, column),
None => {
src.area.press_at(&g, mx, my, shift, now);
}
}
src.focus = true;
}
SourceAction::Save => {
self.save_shader_source();
}
SourceAction::Status => {
if let Some(src) = self.shaders.source.as_mut()
&& let Some((line, column)) = src.jump
{
src.area.go_to(line, column);
src.focus = true;
}
}
SourceAction::ToggleReference => {
self.shaders.reference.open ^= true;
}
SourceAction::Reference(slot) => self.press_reference(slot),
SourceAction::Consume => {
if let Some(src) = self.shaders.source.as_mut() {
src.focus = false;
}
}
}
}
fn press_reference(&mut self, slot: usize) {
let rows = self.reference_rows();
let Some(row) = rows.get(self.shaders.reference.scroll + slot) else {
return;
};
match shader_reference::click(row) {
RefClick::Fold(group) => {
self.shaders.reference.toggle(group);
self.scroll_reference(0);
}
RefClick::Insert(text) => {
if let Some(src) = self.shaders.source.as_mut() {
src.area.insert(&text);
src.focus = true;
}
}
}
}
pub(in crate::editor::hook) fn shader_source_keys(
&mut self,
world: &mut World,
input: &FrameInput,
) {
let Some(g) = self.shader_geometry() else {
return;
};
let typing = self.shader_typing();
let Some(src) = self.shaders.source.as_mut() else {
return;
};
if src.area.pointer_busy() {
src.area
.pointer(&g, input.mouse_x, input.mouse_y, input.left_button_down);
}
if !typing || input.key_events.is_empty() {
return;
}
let clipboard = clipboard::system_or(world, &mut self.text_clipboard);
let response = src
.area
.handle_events(&input.key_events, Platform::current(), clipboard);
if response.save {
self.save_shader_source();
}
}
}