#include "buffers.h" #include "common.h" #include "util.h" #include #include #include #include extern int pclose(FILE *stream); #define BUFFER_INITIAL_CAP 64 static int ecex_trace_callbacks_enabled(void) { const char *v = getenv("ECEX_TRACE_CALLBACKS"); return v && v[0] && v[0] != '0'; } static int buffer_is_word_char(char c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_'; } static int buffer_require_editable(buffer_t *buffer) { if (!buffer || buffer->read_only) return ECEX_ERR; return ECEX_OK; } static void buffer_undo_entry_free(ecex_undo_entry_t *entry) { if (!entry) return; free(entry->data); entry->data = NULL; entry->len = 0; entry->point = 0; entry->mark = 0; entry->mark_active = 0; } static void buffer_undo_stack_clear(ecex_undo_entry_t *stack, size_t count) { if (!stack) return; for (size_t i = 0; i < count; i++) buffer_undo_entry_free(&stack[i]); } static int buffer_push_snapshot_to(ecex_undo_entry_t **stack, size_t *count, size_t *cap, buffer_t *buffer) { if (!stack || !count || !cap || !buffer) return ECEX_ERR; if (ECEX_GROW_ARRAY(*stack, *count, *cap, 64) != ECEX_OK) return ECEX_ERR; ecex_undo_entry_t *entry = &(*stack)[(*count)++]; memset(entry, 0, sizeof(*entry)); entry->data = malloc(buffer->len + 1); if (!entry->data) { (*count)--; return ECEX_ERR; } memcpy(entry->data, buffer->data, buffer->len + 1); entry->len = buffer->len; entry->point = buffer->point; entry->mark = buffer->mark; entry->mark_active = buffer->mark_active; return ECEX_OK; } static void buffer_clear_redo(buffer_t *buffer) { if (!buffer) return; buffer_undo_stack_clear(buffer->redo_stack, buffer->redo_count); buffer->redo_count = 0; } static int buffer_record_undo(buffer_t *buffer) { if (!buffer || buffer->undo_disabled) return ECEX_OK; if (buffer_push_snapshot_to(&buffer->undo_stack, &buffer->undo_count, &buffer->undo_cap, buffer) != ECEX_OK) { return ECEX_ERR; } buffer_clear_redo(buffer); return ECEX_OK; } static int buffer_restore_snapshot(buffer_t *buffer, ecex_undo_entry_t *entry) { if (!buffer || !entry || !entry->data) return ECEX_ERR; char *copy = malloc(entry->len + 1); if (!copy) return ECEX_ERR; memcpy(copy, entry->data, entry->len + 1); free(buffer->data); buffer->data = copy; buffer->len = entry->len; buffer->cap = entry->len + 1; buffer->point = ECEX_MIN(entry->point, buffer->len); buffer->mark = ECEX_MIN(entry->mark, buffer->len); buffer->mark_active = entry->mark_active; buffer->modified = 1; return ECEX_OK; } buffer_t *buffer_new(const char *name, const char *path, int read_only) { assert(name && "buffer name cannot be NULL"); buffer_t *buffer = calloc(1, sizeof(*buffer)); if (!buffer) return NULL; buffer->name = ecex_strdup(name); buffer->path = ecex_strdup(path); buffer->data = malloc(1); if (!buffer->name || (path && !buffer->path) || !buffer->data) { buffer_free(buffer); return NULL; } buffer->data[0] = '\0'; buffer->len = 0; buffer->cap = 1; buffer->read_only = read_only; return buffer; } void buffer_free(buffer_t *buffer) { if (!buffer) return; buffer_clear_interactive_actions(buffer); ecex_buffer_clear_animation(buffer); ecex_buffer_clear_mouse_handler(buffer); ecex_buffer_clear_renderer(buffer); if (buffer->media_pipe) pclose((FILE *)buffer->media_pipe); free(buffer->media_path); free(buffer->media_pixels); buffer_undo_stack_clear(buffer->undo_stack, buffer->undo_count); buffer_undo_stack_clear(buffer->redo_stack, buffer->redo_count); free(buffer->undo_stack); free(buffer->redo_stack); free(buffer->interactive_actions); free(buffer->name); free(buffer->path); free(buffer->data); free(buffer); } int buffer_reserve(buffer_t *buffer, size_t needed) { ECEX_RETURN_ERR_IF_NULL(buffer); if (needed <= buffer->cap) return ECEX_OK; size_t new_cap = buffer->cap ? buffer->cap : BUFFER_INITIAL_CAP; while (new_cap < needed) { new_cap *= 2; } char *new_data = realloc(buffer->data, new_cap); if (!new_data) return ECEX_ERR; buffer->data = new_data; buffer->cap = new_cap; return ECEX_OK; } int buffer_clear(buffer_t *buffer) { if (buffer_require_editable(buffer) != ECEX_OK) return ECEX_ERR; if (buffer_record_undo(buffer) != ECEX_OK) return ECEX_ERR; if (buffer_reserve(buffer, 1) != ECEX_OK) return ECEX_ERR; buffer->data[0] = '\0'; buffer->len = 0; buffer->point = 0; buffer->mark = 0; buffer->mark_active = 0; buffer->scroll_line = 0; buffer->scroll_col = 0; buffer_clear_interactive_actions(buffer); buffer->modified = 1; return ECEX_OK; } int buffer_set_text(buffer_t *buffer, const char *text) { if (buffer_require_editable(buffer) != ECEX_OK || !text) return ECEX_ERR; if (buffer_record_undo(buffer) != ECEX_OK) return ECEX_ERR; size_t len = strlen(text); if (buffer_reserve(buffer, len + 1) != ECEX_OK) return ECEX_ERR; memcpy(buffer->data, text, len + 1); buffer->len = len; buffer->point = len; buffer->mark = 0; buffer->mark_active = 0; buffer->scroll_line = 0; buffer->scroll_col = 0; buffer_clear_interactive_actions(buffer); buffer->modified = 1; return ECEX_OK; } int buffer_insert_at(buffer_t *buffer, size_t pos, const char *text) { if (buffer_require_editable(buffer) != ECEX_OK || !text) return ECEX_ERR; if (pos > buffer->len) return ECEX_ERR; size_t text_len = strlen(text); if (text_len == 0) return ECEX_OK; if (buffer_record_undo(buffer) != ECEX_OK) return ECEX_ERR; size_t needed = buffer->len + text_len + 1; if (buffer_reserve(buffer, needed) != ECEX_OK) return ECEX_ERR; memmove(buffer->data + pos + text_len, buffer->data + pos, buffer->len - pos + 1); memcpy(buffer->data + pos, text, text_len); buffer->len += text_len; buffer->point = pos + text_len; buffer->modified = 1; return ECEX_OK; } int buffer_insert(buffer_t *buffer, const char *text) { ECEX_RETURN_ERR_IF_NULL(buffer); return buffer_insert_at(buffer, buffer->point, text); } int buffer_append(buffer_t *buffer, const char *text) { ECEX_RETURN_ERR_IF_NULL(buffer); return buffer_insert_at(buffer, buffer->len, text); } int buffer_prepend(buffer_t *buffer, const char *text) { return buffer_insert_at(buffer, 0, text); } int buffer_insert_char_at(buffer_t *buffer, size_t pos, char c) { if (buffer_require_editable(buffer) != ECEX_OK) return ECEX_ERR; if (pos > buffer->len) return ECEX_ERR; if (buffer_record_undo(buffer) != ECEX_OK) return ECEX_ERR; if (buffer_reserve(buffer, buffer->len + 2) != ECEX_OK) return ECEX_ERR; memmove(buffer->data + pos + 1, buffer->data + pos, buffer->len - pos + 1); buffer->data[pos] = c; buffer->len++; buffer->point = pos + 1; buffer->modified = 1; return ECEX_OK; } int buffer_insert_char(buffer_t *buffer, char c) { ECEX_RETURN_ERR_IF_NULL(buffer); return buffer_insert_char_at(buffer, buffer->point, c); } int buffer_delete_range(buffer_t *buffer, size_t start, size_t end) { if (buffer_require_editable(buffer) != ECEX_OK) return ECEX_ERR; if (start > end || end > buffer->len) return ECEX_ERR; if (start == end) return ECEX_OK; if (buffer_record_undo(buffer) != ECEX_OK) return ECEX_ERR; size_t deleted = end - start; memmove(buffer->data + start, buffer->data + end, buffer->len - end + 1); buffer->len -= deleted; if (buffer->point > end) { buffer->point -= deleted; } else if (buffer->point > start) { buffer->point = start; } if (buffer->mark > end) { buffer->mark -= deleted; } else if (buffer->mark > start) { buffer->mark = start; } if (buffer->mark_active && buffer->mark == buffer->point) { buffer->mark_active = 0; } buffer->modified = 1; return ECEX_OK; } int buffer_delete_selection(buffer_t *buffer) { if (!buffer_has_selection(buffer)) return ECEX_OK; size_t start = 0; size_t end = 0; buffer_selection_range(buffer, &start, &end); int result = buffer_delete_range(buffer, start, end); if (result == ECEX_OK) buffer_clear_mark(buffer); return result; } int buffer_replace_selection(buffer_t *buffer, const char *text) { if (!buffer || !text) return ECEX_ERR; if (!buffer_has_selection(buffer)) return buffer_insert(buffer, text); size_t start = 0; size_t end = 0; buffer_selection_range(buffer, &start, &end); if (buffer_delete_range(buffer, start, end) != ECEX_OK) return ECEX_ERR; buffer->point = start; buffer_clear_mark(buffer); return buffer_insert(buffer, text); } int buffer_backspace(buffer_t *buffer) { if (!buffer) return ECEX_ERR; if (buffer_has_selection(buffer)) return buffer_delete_selection(buffer); if (buffer->point == 0) return ECEX_OK; return buffer_delete_range(buffer, buffer->point - 1, buffer->point); } int buffer_delete_forward(buffer_t *buffer) { if (!buffer) return ECEX_ERR; if (buffer_has_selection(buffer)) return buffer_delete_selection(buffer); if (buffer->point >= buffer->len) return ECEX_OK; return buffer_delete_range(buffer, buffer->point, buffer->point + 1); } int buffer_kill_line(buffer_t *buffer) { if (!buffer) return ECEX_ERR; size_t end = buffer_line_end_at(buffer, buffer->point); if (end == buffer->point && end < buffer->len && buffer->data[end] == '\n') { end++; } return buffer_delete_range(buffer, buffer->point, end); } void buffer_set_point(buffer_t *buffer, size_t point) { if (!buffer) return; buffer->point = ECEX_MIN(point, buffer->len); } void buffer_move_left(buffer_t *buffer) { if (!buffer || buffer->point == 0) return; buffer->point--; } void buffer_move_right(buffer_t *buffer) { if (!buffer || buffer->point >= buffer->len) return; buffer->point++; } void buffer_move_up(buffer_t *buffer) { if (!buffer) return; size_t current_start = buffer_line_start_at(buffer, buffer->point); if (current_start == 0) { buffer->point = 0; return; } size_t wanted_col = buffer->point - current_start; size_t prev_end = current_start - 1; size_t prev_start = buffer_line_start_at(buffer, prev_end); size_t prev_len = prev_end - prev_start; buffer->point = prev_start + ECEX_MIN(wanted_col, prev_len); } void buffer_move_down(buffer_t *buffer) { if (!buffer) return; size_t current_start = buffer_line_start_at(buffer, buffer->point); size_t current_end = buffer_line_end_at(buffer, buffer->point); if (current_end >= buffer->len) { buffer->point = buffer->len; return; } size_t wanted_col = buffer->point - current_start; size_t next_start = current_end + 1; size_t next_end = buffer_line_end_at(buffer, next_start); size_t next_len = next_end - next_start; buffer->point = next_start + ECEX_MIN(wanted_col, next_len); } void buffer_move_word_left(buffer_t *buffer) { if (!buffer) return; size_t pos = buffer->point; while (pos > 0 && !buffer_is_word_char(buffer->data[pos - 1])) pos--; while (pos > 0 && buffer_is_word_char(buffer->data[pos - 1])) pos--; buffer->point = pos; } void buffer_move_word_right(buffer_t *buffer) { if (!buffer) return; size_t pos = buffer->point; while (pos < buffer->len && !buffer_is_word_char(buffer->data[pos])) pos++; while (pos < buffer->len && buffer_is_word_char(buffer->data[pos])) pos++; buffer->point = pos; } void buffer_move_beginning_of_line(buffer_t *buffer) { if (!buffer) return; buffer->point = buffer_line_start_at(buffer, buffer->point); } void buffer_move_end_of_line(buffer_t *buffer) { if (!buffer) return; buffer->point = buffer_line_end_at(buffer, buffer->point); } void buffer_move_beginning_of_buffer(buffer_t *buffer) { if (!buffer) return; buffer->point = 0; } void buffer_move_end_of_buffer(buffer_t *buffer) { if (!buffer) return; buffer->point = buffer->len; } void buffer_set_mark(buffer_t *buffer, size_t mark) { if (!buffer) return; buffer->mark = ECEX_MIN(mark, buffer->len); buffer->mark_active = 1; } void buffer_clear_mark(buffer_t *buffer) { if (!buffer) return; buffer->mark_active = 0; } int buffer_has_selection(buffer_t *buffer) { return buffer && buffer->mark_active && buffer->mark != buffer->point; } void buffer_selection_range(buffer_t *buffer, size_t *out_start, size_t *out_end) { size_t start = 0; size_t end = 0; if (buffer && buffer_has_selection(buffer)) { start = ECEX_MIN(buffer->point, buffer->mark); end = ECEX_MAX(buffer->point, buffer->mark); } if (out_start) *out_start = start; if (out_end) *out_end = end; } size_t buffer_line_start_at(buffer_t *buffer, size_t pos) { if (!buffer) return 0; pos = ECEX_MIN(pos, buffer->len); while (pos > 0 && buffer->data[pos - 1] != '\n') { pos--; } return pos; } size_t buffer_line_end_at(buffer_t *buffer, size_t pos) { if (!buffer) return 0; pos = ECEX_MIN(pos, buffer->len); while (pos < buffer->len && buffer->data[pos] != '\n') { pos++; } return pos; } size_t buffer_current_line_start(buffer_t *buffer) { if (!buffer) return 0; return buffer_line_start_at(buffer, buffer->point); } size_t buffer_current_line_end(buffer_t *buffer) { if (!buffer) return 0; return buffer_line_end_at(buffer, buffer->point); } size_t buffer_current_column(buffer_t *buffer) { if (!buffer) return 0; return buffer->point - buffer_line_start_at(buffer, buffer->point); } size_t buffer_current_line_number(buffer_t *buffer) { if (!buffer) return 0; size_t line = 1; for (size_t i = 0; i < buffer->point && i < buffer->len; i++) { if (buffer->data[i] == '\n') line++; } return line; } size_t buffer_line_count(buffer_t *buffer) { if (!buffer || buffer->len == 0) return 1; size_t lines = 1; for (size_t i = 0; i < buffer->len; i++) { if (buffer->data[i] == '\n') lines++; } return lines; } char *buffer_substring(buffer_t *buffer, size_t start, size_t end) { if (!buffer || start > end || end > buffer->len) return NULL; size_t len = end - start; char *copy = malloc(len + 1); if (!copy) return NULL; memcpy(copy, buffer->data + start, len); copy[len] = '\0'; return copy; } char *buffer_current_line_copy(buffer_t *buffer) { if (!buffer) return NULL; return buffer_substring(buffer, buffer_current_line_start(buffer), buffer_current_line_end(buffer)); } int buffer_load_file(buffer_t *buffer, const char *path) { if (buffer_require_editable(buffer) != ECEX_OK || !path) return ECEX_ERR; size_t size = 0; char *new_data = ecex_read_entire_file(path, &size); if (!new_data) return ECEX_ERR; char *new_path = ecex_strdup(path); if (!new_path) { free(new_data); return ECEX_ERR; } free(buffer->data); free(buffer->path); buffer->data = new_data; buffer->path = new_path; buffer->len = size; buffer->cap = size + 1; buffer->point = 0; buffer->mark = 0; buffer->mark_active = 0; buffer->scroll_line = 0; buffer->scroll_col = 0; buffer->modified = 0; buffer_clear_undo(buffer); return ECEX_OK; } int buffer_save(buffer_t *buffer) { if (!buffer || buffer->read_only || !buffer->path) return ECEX_ERR; FILE *file = fopen(buffer->path, "wb"); if (!file) return ECEX_ERR; size_t written = fwrite(buffer->data, 1, buffer->len, file); int close_result = fclose(file); if (close_result != 0 || written != buffer->len) return ECEX_ERR; buffer->modified = 0; return ECEX_OK; } int buffer_save_as(buffer_t *buffer, const char *path) { if (buffer_require_editable(buffer) != ECEX_OK || !path) return ECEX_ERR; char *new_path = ecex_strdup(path); if (!new_path) return ECEX_ERR; free(buffer->path); buffer->path = new_path; return buffer_save(buffer); } int buffer_undo(buffer_t *buffer) { if (buffer_require_editable(buffer) != ECEX_OK) return ECEX_ERR; if (buffer->undo_count == 0) return ECEX_OK; if (buffer_push_snapshot_to(&buffer->redo_stack, &buffer->redo_count, &buffer->redo_cap, buffer) != ECEX_OK) return ECEX_ERR; ecex_undo_entry_t entry = buffer->undo_stack[--buffer->undo_count]; int result = buffer_restore_snapshot(buffer, &entry); buffer_undo_entry_free(&entry); return result; } int buffer_redo(buffer_t *buffer) { if (buffer_require_editable(buffer) != ECEX_OK) return ECEX_ERR; if (buffer->redo_count == 0) return ECEX_OK; if (buffer_push_snapshot_to(&buffer->undo_stack, &buffer->undo_count, &buffer->undo_cap, buffer) != ECEX_OK) return ECEX_ERR; ecex_undo_entry_t entry = buffer->redo_stack[--buffer->redo_count]; int result = buffer_restore_snapshot(buffer, &entry); buffer_undo_entry_free(&entry); return result; } void buffer_clear_undo(buffer_t *buffer) { if (!buffer) return; buffer_undo_stack_clear(buffer->undo_stack, buffer->undo_count); buffer_undo_stack_clear(buffer->redo_stack, buffer->redo_count); buffer->undo_count = 0; buffer->redo_count = 0; } int buffer_search_forward(buffer_t *buffer, const char *query, size_t start, size_t *out_pos) { if (!buffer || !query || !query[0]) return ECEX_ERR; if (start > buffer->len) start = buffer->len; char *hit = strstr(buffer->data + start, query); if (!hit && start > 0) hit = strstr(buffer->data, query); if (!hit) return ECEX_ERR; if (out_pos) *out_pos = (size_t)(hit - buffer->data); return ECEX_OK; } int buffer_search_backward(buffer_t *buffer, const char *query, size_t start, size_t *out_pos) { if (!buffer || !query || !query[0]) return ECEX_ERR; size_t qlen = strlen(query); if (qlen > buffer->len) return ECEX_ERR; if (start > buffer->len) start = buffer->len; size_t best = (size_t)-1; for (size_t i = 0; i + qlen <= start; i++) { if (memcmp(buffer->data + i, query, qlen) == 0) best = i; } if (best == (size_t)-1) { for (size_t i = start; i + qlen <= buffer->len; i++) { if (memcmp(buffer->data + i, query, qlen) == 0) best = i; } } if (best == (size_t)-1) return ECEX_ERR; if (out_pos) *out_pos = best; return ECEX_OK; } void buffer_set_interactive(buffer_t *buffer, int interactive) { if (!buffer) return; buffer->interactive = interactive ? 1 : 0; } int buffer_is_interactive(buffer_t *buffer) { return buffer && buffer->interactive; } int buffer_clear_interactive_actions(buffer_t *buffer) { if (!buffer) return ECEX_ERR; for (size_t i = 0; i < buffer->interactive_action_count; i++) { free(buffer->interactive_actions[i].payload); buffer->interactive_actions[i].payload = NULL; buffer->interactive_actions[i].fn = NULL; buffer->interactive_actions[i].userdata = NULL; buffer->interactive_actions[i].line = 0; } buffer->interactive_action_count = 0; return ECEX_OK; } int buffer_add_interactive_action(buffer_t *buffer, size_t line, ecex_interactive_line_fn fn, const char *payload, void *userdata) { if (!buffer || !fn) return ECEX_ERR; if (ECEX_GROW_ARRAY(buffer->interactive_actions, buffer->interactive_action_count, buffer->interactive_action_cap, 16) != ECEX_OK) { return ECEX_ERR; } char *payload_copy = ecex_strdup(payload); if (payload && !payload_copy) return ECEX_ERR; ecex_interactive_line_action_t *action = &buffer->interactive_actions[buffer->interactive_action_count++]; action->line = line; action->fn = fn; action->payload = payload_copy; action->userdata = userdata; buffer->interactive = 1; return ECEX_OK; } ecex_interactive_line_action_t *buffer_interactive_action_at_line(buffer_t *buffer, size_t line) { if (!buffer || !buffer->interactive) return NULL; for (size_t i = 0; i < buffer->interactive_action_count; i++) { if (buffer->interactive_actions[i].line == line) { return &buffer->interactive_actions[i]; } } return NULL; } int ecex_buffer_set_renderer(buffer_t *buffer, ecex_buffer_render_fn fn, void *userdata, ecex_buffer_userdata_free_fn free_fn, int flags) { if (ecex_trace_callbacks_enabled()) { ecex_logf("buffer_set_renderer buffer=%p fn=%p userdata=%p free=%p flags=%d", (void *)buffer, (void *)fn, userdata, (void *)free_fn, flags); } if (!buffer) return ECEX_ERR; if (buffer->render_userdata_free && buffer->render_userdata && buffer->render_userdata != userdata) { buffer->render_userdata_free(buffer->render_userdata); } buffer->render_fn = fn; buffer->render_userdata = userdata; buffer->render_userdata_free = free_fn; buffer->render_flags = flags; return ECEX_OK; } int ecex_buffer_clear_renderer(buffer_t *buffer) { if (ecex_trace_callbacks_enabled()) { ecex_logf("buffer_clear_renderer buffer=%p userdata=%p free=%p", (void *)buffer, buffer ? buffer->render_userdata : NULL, buffer ? (void *)buffer->render_userdata_free : NULL); } if (!buffer) return ECEX_ERR; if (buffer->render_userdata_free && buffer->render_userdata) { buffer->render_userdata_free(buffer->render_userdata); } buffer->render_fn = NULL; buffer->render_userdata = NULL; buffer->render_userdata_free = NULL; buffer->render_flags = 0; return ECEX_OK; } int ecex_buffer_has_renderer(buffer_t *buffer) { return buffer && buffer->render_fn; } void *ecex_buffer_renderer_userdata(buffer_t *buffer) { return buffer ? buffer->render_userdata : NULL; } int ecex_buffer_set_mouse_handler(buffer_t *buffer, ecex_buffer_mouse_fn fn, void *userdata, ecex_buffer_userdata_free_fn free_fn) { if (ecex_trace_callbacks_enabled()) { ecex_logf("buffer_set_mouse_handler buffer=%p fn=%p userdata=%p free=%p", (void *)buffer, (void *)fn, userdata, (void *)free_fn); } if (!buffer || !fn) return ECEX_ERR; if (buffer->mouse_userdata_free && buffer->mouse_userdata && buffer->mouse_userdata != userdata) { buffer->mouse_userdata_free(buffer->mouse_userdata); } buffer->mouse_fn = fn; buffer->mouse_userdata = userdata; buffer->mouse_userdata_free = free_fn; return ECEX_OK; } int ecex_buffer_clear_mouse_handler(buffer_t *buffer) { if (ecex_trace_callbacks_enabled()) { ecex_logf("buffer_clear_mouse_handler buffer=%p userdata=%p free=%p", (void *)buffer, buffer ? buffer->mouse_userdata : NULL, buffer ? (void *)buffer->mouse_userdata_free : NULL); } if (!buffer) return ECEX_ERR; if (buffer->mouse_userdata_free && buffer->mouse_userdata) { buffer->mouse_userdata_free(buffer->mouse_userdata); } buffer->mouse_fn = NULL; buffer->mouse_userdata = NULL; buffer->mouse_userdata_free = NULL; return ECEX_OK; } int ecex_buffer_has_mouse_handler(buffer_t *buffer) { return buffer && buffer->mouse_fn; } void *ecex_buffer_mouse_userdata(buffer_t *buffer) { return buffer ? buffer->mouse_userdata : NULL; } int ecex_buffer_set_animation(buffer_t *buffer, ecex_buffer_tick_fn fn, void *userdata, ecex_buffer_userdata_free_fn free_fn, double fps) { if (ecex_trace_callbacks_enabled()) { ecex_logf("buffer_set_animation buffer=%p fn=%p userdata=%p free=%p fps=%.3f", (void *)buffer, (void *)fn, userdata, (void *)free_fn, fps); } if (!buffer || !fn) return ECEX_ERR; if (buffer->tick_userdata_free && buffer->tick_userdata && buffer->tick_userdata != userdata) { buffer->tick_userdata_free(buffer->tick_userdata); } if (fps <= 0.0) fps = 60.0; if (fps > 240.0) fps = 240.0; buffer->tick_fn = fn; buffer->tick_ms_fn = NULL; buffer->tick_userdata = userdata; buffer->tick_userdata_free = free_fn; buffer->tick_interval = 1.0 / fps; buffer->tick_last_time = 0.0; buffer->tick_enabled = 1; buffer->tick_uses_ms = 0; return ECEX_OK; } int ecex_buffer_set_animation_ms(buffer_t *buffer, ecex_buffer_tick_ms_fn fn, void *userdata, ecex_buffer_userdata_free_fn free_fn, int fps) { if (ecex_trace_callbacks_enabled()) { ecex_logf("buffer_set_animation_ms buffer=%p fn=%p userdata=%p free=%p fps=%d", (void *)buffer, (void *)fn, userdata, (void *)free_fn, fps); } if (!buffer || !fn) return ECEX_ERR; if (buffer->tick_userdata_free && buffer->tick_userdata && buffer->tick_userdata != userdata) { buffer->tick_userdata_free(buffer->tick_userdata); } if (fps <= 0) fps = 60; if (fps > 240) fps = 240; buffer->tick_fn = NULL; buffer->tick_ms_fn = fn; buffer->tick_userdata = userdata; buffer->tick_userdata_free = free_fn; buffer->tick_interval = 1.0 / (double)fps; buffer->tick_last_time = 0.0; buffer->tick_enabled = 1; buffer->tick_uses_ms = 1; return ECEX_OK; } int ecex_buffer_clear_animation(buffer_t *buffer) { if (ecex_trace_callbacks_enabled()) { ecex_logf("buffer_clear_animation buffer=%p userdata=%p free=%p", (void *)buffer, buffer ? buffer->tick_userdata : NULL, buffer ? (void *)buffer->tick_userdata_free : NULL); } if (!buffer) return ECEX_ERR; if (buffer->tick_userdata_free && buffer->tick_userdata) { buffer->tick_userdata_free(buffer->tick_userdata); } buffer->tick_fn = NULL; buffer->tick_ms_fn = NULL; buffer->tick_userdata = NULL; buffer->tick_userdata_free = NULL; buffer->tick_interval = 0.0; buffer->tick_last_time = 0.0; buffer->tick_enabled = 0; buffer->tick_uses_ms = 0; return ECEX_OK; } int ecex_buffer_is_animating(buffer_t *buffer) { return buffer && buffer->tick_enabled && (buffer->tick_fn || buffer->tick_ms_fn); } void *ecex_buffer_animation_userdata(buffer_t *buffer) { return buffer ? buffer->tick_userdata : NULL; } int ecex_tick_animations(ecex_t *ed, double now_seconds) { if (!ed) return 0; int dirty = 0; int frame_group_open = 0; int trace_callbacks = ecex_trace_callbacks_enabled(); for (size_t i = 0; i < ed->buffer_count; i++) { buffer_t *buffer = ed->buffers[i]; if (!buffer || !buffer->tick_enabled) continue; if (buffer->tick_uses_ms) { if (!buffer->tick_ms_fn) { if (trace_callbacks) { if (!frame_group_open) { char frame_msg[128]; snprintf(frame_msg, sizeof(frame_msg), "animation frame start now=%.6f", now_seconds); ecex_log_group_begin(frame_msg); frame_group_open = 1; } ecex_logf("animation_tick_skip_ms_null buffer=%p userdata=%p", (void *)buffer, buffer->tick_userdata); } buffer->tick_enabled = 0; continue; } } else { if (!buffer->tick_fn) { if (trace_callbacks) { if (!frame_group_open) { char frame_msg[128]; snprintf(frame_msg, sizeof(frame_msg), "animation frame start now=%.6f", now_seconds); ecex_log_group_begin(frame_msg); frame_group_open = 1; } ecex_logf("animation_tick_skip_null buffer=%p userdata=%p", (void *)buffer, buffer->tick_userdata); } buffer->tick_enabled = 0; continue; } } if (buffer->tick_last_time <= 0.0) { buffer->tick_last_time = now_seconds; } double interval = buffer->tick_interval > 0.0 ? buffer->tick_interval : (1.0 / 60.0); if (now_seconds - buffer->tick_last_time + 0.000001 < interval) { continue; } buffer->tick_last_time = now_seconds; int tick_dirty = 0; if (!frame_group_open) { char frame_msg[128]; snprintf(frame_msg, sizeof(frame_msg), "animation frame start now=%.6f", now_seconds); ecex_log_group_begin(frame_msg); frame_group_open = 1; } if (buffer->tick_uses_ms) { int now_ms = (int)(now_seconds * 1000.0); if (trace_callbacks) { char tick_msg[256]; snprintf(tick_msg, sizeof(tick_msg), "animation tick ms start buffer=%p fn=%p userdata=%p now_ms=%d", (void *)buffer, (void *)buffer->tick_ms_fn, buffer->tick_userdata, now_ms); ecex_log_group_begin(tick_msg); } tick_dirty = buffer->tick_ms_fn(ed, buffer, now_ms, buffer->tick_userdata); if (trace_callbacks) { char tick_msg[128]; snprintf(tick_msg, sizeof(tick_msg), "animation tick ms end buffer=%p result=%d", (void *)buffer, tick_dirty); ecex_log_group_end(tick_msg); } } else { if (trace_callbacks) { char tick_msg[256]; snprintf(tick_msg, sizeof(tick_msg), "animation tick start buffer=%p fn=%p userdata=%p now=%.6f", (void *)buffer, (void *)buffer->tick_fn, buffer->tick_userdata, now_seconds); ecex_log_group_begin(tick_msg); } tick_dirty = buffer->tick_fn(ed, buffer, now_seconds, buffer->tick_userdata); if (trace_callbacks) { char tick_msg[128]; snprintf(tick_msg, sizeof(tick_msg), "animation tick end buffer=%p result=%d", (void *)buffer, tick_dirty); ecex_log_group_end(tick_msg); } } if (tick_dirty != 0) { dirty = 1; } } if (frame_group_open) { ecex_log_group_end(dirty ? "animation frame end dirty=1" : "animation frame end dirty=0"); } return dirty; } int ecex_buffer_replace_text(buffer_t *buffer, const char *text) { return buffer_set_text(buffer, text ? text : ""); } void ecex_buffer_set_modified(buffer_t *buffer, int modified) { if (buffer) buffer->modified = modified ? 1 : 0; } int ecex_buffer_text_len(buffer_t *buffer) { if (!buffer) return 0; return buffer->len > (size_t)2147483647 ? 2147483647 : (int)buffer->len; } int ecex_buffer_scroll_line_index(buffer_t *buffer) { if (!buffer) return 0; return buffer->scroll_line > (size_t)2147483647 ? 2147483647 : (int)buffer->scroll_line; } int ecex_buffer_line_count_int(buffer_t *buffer) { size_t n = buffer_line_count(buffer); return n > (size_t)2147483647 ? 2147483647 : (int)n; } int ecex_buffer_line_copy_text(buffer_t *buffer, int line, char *out, int out_cap) { size_t current = 0; size_t pos = 0; size_t start; size_t end; size_t n; if (!out || out_cap <= 0) return ECEX_ERR; out[0] = '\0'; if (!buffer || !buffer->data || line < 0) return ECEX_ERR; while (current < (size_t)line && pos < buffer->len) { if (buffer->data[pos++] == '\n') current++; } if (current != (size_t)line) return ECEX_ERR; start = pos; while (pos < buffer->len && buffer->data[pos] != '\n') pos++; end = pos; while (end > start && (buffer->data[end - 1] == '\n' || buffer->data[end - 1] == '\r')) end--; n = end - start; if (n >= (size_t)out_cap) n = (size_t)out_cap - 1; if (n > 0) memcpy(out, buffer->data + start, n); out[n] = '\0'; return (int)n; }