(in-package :cl-tty.dialog) (defvar *dialog-stack* nil "Stack of active dialogs. (list) of dialog instances.") (defvar *toasts* nil "List of active toast notifications.") (defclass dialog () ((title :initarg :title :accessor dialog-title) (size :initarg :size :initform :medium :accessor dialog-size) (content :initarg :content :initform nil :accessor dialog-content) (on-dismiss :initarg :on-dismiss :initform nil :accessor dialog-on-dismiss))) (defun dialog-size-pixels (size &optional (max-w 80) (max-h 24)) (multiple-value-bind (dw dh) (case size (:small (values 40 8)) (:medium (values 60 16)) (:large (values 88 24)) (t (values 60 16))) (values (min dw max-w) (min dh max-h)))) (defun render-dialog (dialog screen w h) (multiple-value-bind (dw dh) (dialog-size-pixels (dialog-size dialog) w h) (let ((x (floor (- w dw) 2)) (y (floor (- h dh) 2))) ;; Backdrop — dim the full screen (dotimes (row h) (draw-rect screen 0 row w 1 :bg :bright-black)) ;; Dialog panel (draw-border screen x y dw dh :style :single :title (dialog-title dialog)) (when (dialog-content dialog) ;; Content rendering delegated to component system (draw-text screen (1+ x) (1+ y) (format nil "~a" (dialog-content dialog)) :white :default))))) (defun push-dialog (dialog) (push dialog *dialog-stack*) dialog) (defun pop-dialog () (when *dialog-stack* (let ((dialog (pop *dialog-stack*))) (when (dialog-on-dismiss dialog) (funcall (dialog-on-dismiss dialog))) dialog))) (defun alert-dialog (title message) (make-instance 'dialog :title title :size :small :content (make-instance 'select :options (list (list :title "OK" :value :ok)) :on-select (lambda (opt) (declare (ignore opt)) (pop-dialog))) :on-dismiss (lambda () (pop-dialog)))) (defun confirm-dialog (title message &key on-yes on-no) (make-instance 'dialog :title title :size :small :content (make-instance 'select :options (list (list :title "Yes" :value :yes) (list :title "No" :value :no)) :on-select (lambda (opt) (pop-dialog) (if (eql opt :yes) (when on-yes (funcall on-yes)) (when on-no (funcall on-no))))))) (defun select-dialog (title options &key on-select) (make-instance 'dialog :title title :size :medium :content (make-instance 'select :options options :on-select (lambda (opt) (pop-dialog) (when on-select (funcall on-select opt)))))) (defun prompt-dialog (title &key on-submit) (make-instance 'dialog :title title :size :small :content (make-instance 'text-input :on-submit (lambda (value) (pop-dialog) (when on-submit (funcall on-submit value)))))) (defclass toast () ((message :initarg :message :accessor toast-message) (variant :initarg :variant :initform :info :accessor toast-variant))) (defun render-toast (toast screen w) (let* ((msg (toast-message toast)) (variant (toast-variant toast)) (color (case variant (:info :blue) (:success :green) (:warning :yellow) (:error :red))) (max-w (min 60 (1- w))) (x (- w max-w 1)) (text (if (> (length msg) (- max-w 2)) (concatenate 'string (subseq msg 0 (- max-w 5)) "...") msg))) (draw-rect screen x 0 max-w 1 :bg color) (draw-text screen (1+ x) 0 text :white color :bold t))) (defun toast (message &key (variant :info) (duration 0)) (let ((toast (make-instance 'toast :message message :variant variant))) (push toast *toasts*) (when (plusp duration) (dismiss-toast toast)) toast)) (defun dismiss-toast (toast) (setf *toasts* (remove toast *toasts*)))