您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
An IMGUI inspired GUI Framework for javascript thats designed to be as simple to use as IMGUI.
当前为
此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.greasyfork.icu/scripts/535798/1591785/ImmediateGUI.js
class ImmediateGUI { static OccupiedElementIds = []; static GenerateId(prefix = "gui_") { if (typeof prefix !== 'string' || prefix.length === 0) { prefix = "gui_"; } const timestamp = Date.now().toString(36); const randomPart = Math.random().toString(36).substring(2, 15); const generatedId = prefix + timestamp + randomPart; const exists = ImmediateGUI.OccupiedElementIds.includes(generatedId); if (exists) return ImmediateGUI.GenerateId(prefix); ImmediateGUI.OccupiedElementIds.push(generatedId); return generatedId; } constructor(options = {}) { this.options = { theme: 'dark', position: 'right', width: 300, draggable: true, title: 'Immediate GUI', titleLeftAligned: true, ...options }; this.themes = { light: { background: '#ffffff', text: '#333333', border: '#cccccc', accent: '#4285f4', buttonBg: '#f5f5f5', buttonHover: '#e0e0e0', inputBg: '#ffffff', sectionBg: '#f9f9f9' }, dark: { // background: '#2d2d2d', background: '#151617', // text: '#e0e0e0', text: '#eef0f2', // border: '#555555', border: '#425069', // accent: '#4d90fe', accent: '#294a7a', // buttonBg: '#444444', buttonBg: '#274972', // buttonHover: '#555555', buttonHover: '#336caf', // inputBg: '#3d3d3d', inputBg: '#20324d', // sectionBg: '#333333' sectionBg: '#232426', } }; this.maxHeight = '85vh'; //this.maxHeight = 'auto;'; this.theme = this.themes[this.options.theme] || this.themes.light; // Create main container this.container = document.createElement('div'); this.container.id = ImmediateGUI.GenerateId(); this._applyGlobalStyles(); this._updateCSSVariables(); this.container.style.cssText = ` position: fixed; ${this.options.position === 'right' ? 'right' : 'left'}: 10px; top: 10px; min-width: ${(typeof this.options.width === 'string' ? this.options.width : this.options.width) + 'px'}; background: var(--imgui-bg); color: var(--imgui-text); border: 1px solid var(--imgui-border); border-radius: 4px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif; -webkit-font-smoothing: antialiased; font-size: 14px; z-index: 9999; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); padding: 0; /* Remove all padding */ max-height: ${this.maxHeight}; overflow-y: auto; overflow-x: hidden; transition: all 0.2s ease; `; this._createTitleBar(this.options.titleLeftAligned); this.contentContainer = document.createElement('div'); this.contentContainer.id = ImmediateGUI.GenerateId('content_'); this.contentContainer.style.cssText = ` width: 100%; box-sizing: border-box; padding: 0 12px 12px 12px; /* Add padding to content area only */ `; this.container.appendChild(this.contentContainer); this.currentSection = null; this.indentationLevel = 0; this.indentationSize = 10; // pixels per level this.isCustomIndentationLevel = false; if (this.options.draggable) { this._setupDragging(); } } _createTitleBar(leftAlign = true) { this.titleBar = document.createElement('div'); this.titleBar.className = 'imgui-titlebar'; this.titleBar.style.cssText = ` width: 100% !important; padding-top: var(--imgui-bottom-padding); background: var(--imgui-section-bg); color: var(--imgui-text); font-weight: bold; cursor: ${this.options.draggable ? 'pointer' : 'default'}; user-select: none; box-sizing: border-box; position: sticky; top: 0; z-index: 999999999; margin-bottom: 12px; border-bottom: 1px solid var(--imgui-border); ${leftAlign ? 'text-align: left; padding-left: 12px;' : 'text-align: center;'} height: var(--imgui-title-height); `; this.titleBar.textContent = this.options.title || 'ImmediateGUI'; this.container.appendChild(this.titleBar); } _updateCSSVariables() { document.documentElement.style.setProperty('--imgui-bg', this.theme.background); document.documentElement.style.setProperty('--imgui-text', this.theme.text); document.documentElement.style.setProperty('--imgui-border', this.theme.border); document.documentElement.style.setProperty('--imgui-accent', this.theme.accent); document.documentElement.style.setProperty('--imgui-button-bg', this.theme.buttonBg); document.documentElement.style.setProperty('--imgui-button-hover', this.theme.buttonHover); document.documentElement.style.setProperty('--imgui-input-bg', this.theme.inputBg); document.documentElement.style.setProperty('--imgui-section-bg', this.theme.sectionBg); } _applyGlobalStyles() { const styleSheetId = `imgui-global-styles_${this.container.id}`; if (!document.getElementById(styleSheetId)) { const styleEl = document.createElement('style'); styleEl.id = styleSheetId; styleEl.textContent = ` :root { --imgui-bg: ${this.theme.background}; --imgui-text: ${this.theme.text}; --imgui-border: ${this.theme.border}; --imgui-accent: ${this.theme.accent}; --imgui-button-bg: ${this.theme.buttonBg}; --imgui-button-hover: ${this.theme.buttonHover}; --imgui-input-bg: ${this.theme.inputBg}; --imgui-section-bg: ${this.theme.sectionBg}; --imgui-bottom-padding: 3px; --imgui-title-height: 27px; --imgui-scrollbar-width: 0.5em; } #${this.container.id} { scrollbar-gutter: auto; /* scrollbar-color: var(--imgui-accent); */ /* scrollbar-width: thin; */ &::-webkit-scrollbar { width: var(--imgui-scrollbar-width); height: 0.5em; background-color: var(--imgui-section-bg); } &::-webkit-scrollbar-thumb { background-color: var(--imgui-accent); /* border-radius: 3px; */ } /* This positions the scrollbar to start below the title bar */ &::-webkit-scrollbar-button:start:decrement { height: var(--imgui-title-height); display: block; background-color: transparent; } } .imgui-titlebar { width: 100% !important; box-sizing: border-box; right: 0; left: 0; } .imgui-progressbar { } .imgui-slider { accent-color: var(--imgui-accent); } .imgui-slider:hover { accent-color: var(--imgui-button-hover); } .imgui-control { margin-bottom: 2px; width: 100%; box-sizing: border-box; } .imgui-button { background: var(--imgui-button-bg); color: var(--imgui-text); border: 1px solid var(--imgui-border); border-radius: 4px; padding: 8px 12px; font-size: 14px; cursor: pointer; transition: all 0.2s ease; outline: none; width: auto; font-family: inherit; margin-right: 5px; } .imgui-button:hover { background: var(--imgui-button-hover); } .imgui-button:active { transform: translateY(1px); } .imgui-input { background: var(--imgui-input-bg); color: var(--imgui-text); border: 1px solid var(--imgui-border); border-radius: 4px; padding: 8px 10px; font-size: 14px; width: 100%; box-sizing: border-box; outline: none; transition: border-color 0.2s ease; font-family: inherit; } .imgui-input::placeholder { color: var(--imgui-text); opacity: 0.5; } .imgui-input:focus { border-color: var(--imgui-accent); } .imgui-section { border: 1px solid var(--imgui-border); border-radius: 4px; padding: 10px 10px 0px; margin-bottom: 12px; background: var(--imgui-section-bg); } .imgui-section-header { font-weight: 600; margin-bottom: 8px; padding-bottom: 6px; border-bottom: 1px solid var(--imgui-border); color: var(--imgui-text); } .imgui-label { display: block; margin-bottom: 4px; color: var(--imgui-text); font-weight: 500; } `; document.head.appendChild(styleEl); this.container.classList.add('imgui-container'); } } _setupDragging() { let isDragging = false; let startX, startY, startLeft, startTop; const isClickOnControl = (element) => { if (!element) return false; return !element.classList.contains('imgui-titlebar'); let current = element; while (current && current !== this.container) { if ( /* current.classList.contains('imgui-control') || */ current.tagName === 'INPUT' || current.tagName === 'BUTTON' || current.tagName === 'SELECT' || current.tagName === 'TEXTAREA' ) { return true; } current = current.parentElement; } return false; }; this.container.addEventListener('mousedown', (e) => { if (e.button !== 0) return; if (isClickOnControl(e.target)) { return; } isDragging = true; startX = e.clientX; startY = e.clientY; const rect = this.container.getBoundingClientRect(); startLeft = rect.left; startTop = rect.top; document.body.style.cursor = 'move'; e.preventDefault(); }); document.addEventListener('mousemove', (e) => { if (!isDragging) return; const dx = e.clientX - startX; const dy = e.clientY - startY; const newLeft = startLeft + dx; const newTop = startTop + dy; this.container.style.left = `${newLeft}px`; this.container.style.top = `${newTop}px`; this.container.style.right = 'auto'; }); document.addEventListener('mouseup', () => { if (isDragging) { isDragging = false; document.body.style.cursor = ''; this._keepInView(); } }); document.addEventListener('mouseleave', () => { if (isDragging) { isDragging = false; document.body.style.cursor = ''; } }); } _keepInView() { const rect = this.container.getBoundingClientRect(); const windowWidth = window.innerWidth; const windowHeight = window.innerHeight; const minVisiblePx = 50; let newLeft = rect.left; let newTop = rect.top; if (rect.right < minVisiblePx) { newLeft = minVisiblePx - rect.width; } else if (rect.left > windowWidth - minVisiblePx) { newLeft = windowWidth - minVisiblePx; } if (rect.bottom < minVisiblePx) { newTop = minVisiblePx - rect.height; } else if (rect.top > windowHeight - minVisiblePx) { newTop = windowHeight - minVisiblePx; } if (newLeft !== rect.left || newTop !== rect.top) { this.container.style.left = `${newLeft}px`; this.container.style.top = `${newTop}px`; } } // Section management BeginSection(title, collapsible = false, collapsedByDefault = false) { const section = document.createElement('div'); section.className = 'imgui-section'; section.id = ImmediateGUI.GenerateId('section_'); section.style.paddingBottom = 'var(--imgui-bottom-padding)'; if (title) { const header = document.createElement('div'); header.className = 'imgui-section-header'; header.style.color = `var(--imgui-text)`; header.style.borderBottom = `1px solid var(--imgui-border)`; if (collapsible) { header.style.cssText = ` display: flex; align-items: center; cursor: pointer; user-select: none; margin-bottom: 8px; padding-bottom: 6px; `; const indicator = document.createElement('span'); indicator.className = 'imgui-section-indicator'; indicator.textContent = '▼'; indicator.style.cssText = ` margin-right: 8px; font-size: 10px; transition: transform 0.2s ease; `; const titleSpan = document.createElement('span'); titleSpan.textContent = title; titleSpan.style.flex = '1'; const content = document.createElement('div'); content.className = 'imgui-section-content'; content.style.cssText = ` overflow: hidden; transition: max-height 0.3s ease; `; section.isCollapsed = false; const toggleCollapse = () => { section.isCollapsed = !section.isCollapsed; if (section.isCollapsed) { content.style.maxHeight = '0px'; indicator.textContent = '►'; // TODO: figure out why the ► character looks squished and skinny?? //indicator.style.fontSize = '14px'; indicator.style.transform = 'rotate(0deg)'; } else { content.style.maxHeight = '2000px'; indicator.textContent = '▼'; indicator.style.transform = 'rotate(0deg)'; } }; if (collapsedByDefault) toggleCollapse(); header.addEventListener('click', toggleCollapse); header.appendChild(indicator); header.appendChild(titleSpan); section.appendChild(header); section.appendChild(content); section.contentContainer = content; } else { header.textContent = title; section.appendChild(header); } } this._getTargetContainer().appendChild(section); this.currentSection = section; return this; } EndSection() { this.currentSection = null; return this; } // Indentation management BeginIndentation(level = -1) { if (level === -1) this.indentationLevel++; else { this.isCustomIndentationLevel = true; this.indentationLevel = level; } return this; } EndIndentation() { if (this.indentationLevel > 0) { if (this.isCustomIndentationLevel) { this.indentationLevel = 0; this.isCustomIndentationLevel = false; } else this.indentationLevel--; } return this; } _applyIndentation(element) { if (this.indentationLevel > 0) { const currentIndent = this.indentationLevel * this.indentationSize; element.style.marginLeft = `${currentIndent}px`; } return element; } // Utility to get current target container _getTargetContainer() { // if (this.currentSection) { // // If current section is collapsible, use its content container // if (this.currentSection.contentContainer) { // return this.currentSection.contentContainer; // } // return this.currentSection; // } // return this.container; if (this.currentSection) { // If current section is collapsible, use its content container if (this.currentSection.contentContainer) { return this.currentSection.contentContainer; } return this.currentSection; } return this.contentContainer; // Changed from this.container } // Original API methods with improved implementation GetControlContainer() { return this.container; } GetControls() { return this.GetControlContainer().querySelectorAll('.imgui-wrapper'); } Separator() { const wrapper = document.createElement('div'); wrapper.className = "imgui-control imgui-wrapper"; wrapper.style.cssText = ` display: flex; width: 100%; margin-bottom: var(--imgui-bottom-padding); `; const separator = document.createElement('hr'); separator.id = ImmediateGUI.GenerateId("ctrl_"); separator.style.cssText = ` border: none; border-top: 1px solid ${this.theme.border}; margin: 10px 0; width: 100%; `; wrapper.appendChild(separator); this._applyIndentation(wrapper); this._getTargetContainer().appendChild(wrapper); return separator; } Header(text, level = 1) { const wrapper = document.createElement('div'); wrapper.className = "imgui-control imgui-wrapper"; wrapper.style.cssText = ` display: flex; width: 100%; margin-bottom: var(--imgui-bottom-padding); `; const validLevel = Math.min(Math.max(level, 1), 6); const header = document.createElement(`h${validLevel}`); header.id = ImmediateGUI.GenerateId("ctrl_"); header.textContent = text; header.style.cssText = ` margin: 0 0 10px 0; padding: 0; font-weight: ${validLevel <= 2 ? 'bold' : '600'}; color: ${this.theme.text}; font-size: ${24 - (validLevel * 2)}px; font-family: inherit; width: 100%; `; wrapper.appendChild(header); this._applyIndentation(wrapper); this._getTargetContainer().appendChild(wrapper); return header; } Button(text, callback, tooltip = '') { const wrapper = document.createElement('div'); wrapper.className = "imgui-control imgui-wrapper"; wrapper.style.cssText = ` display: flex; margin-bottom: var(--imgui-bottom-padding); `; const btn = document.createElement('button'); btn.id = ImmediateGUI.GenerateId("ctrl_"); btn.textContent = text; btn.className = "imgui-button imgui-control"; if (typeof tooltip === 'string' && tooltip.length > 0) btn.title = tooltip; btn.addEventListener('mouseenter', () => { //btn.style.background = this.theme.buttonHover; btn.style.background = 'var(--imgui-button-hover)'; }) btn.addEventListener('mouseout', () => { //btn.style.background = this.theme.buttonBg; btn.style.background = 'var(--imgui-button-bg)'; }) if (callback && typeof callback === 'function') { btn.addEventListener('click', callback); } wrapper.appendChild(btn); this._applyIndentation(wrapper); this._getTargetContainer().appendChild(wrapper); return btn; } Textbox(placeholder, defaultValue = "", tooltip = '') { const wrapper = document.createElement('div'); wrapper.className = "imgui-control imgui-wrapper"; wrapper.style.cssText = ` display: flex; flex-direction: column; margin-bottom: var(--imgui-bottom-padding); `; const input = document.createElement('input'); input.id = ImmediateGUI.GenerateId("ctrl_"); input.type = 'text'; input.placeholder = placeholder; input.value = defaultValue; input.style.background = 'var(--imgui-input-bg)'; input.style.border = '1px solid var(--imgui-border)'; input.className = "imgui-input imgui-control"; if (tooltip && typeof tooltip === 'string' && tooltip.length > 0) input.title = tooltip; wrapper.appendChild(input); this._applyIndentation(wrapper); this._getTargetContainer().appendChild(wrapper); return input; } TextArea(placeholder = "", defaultValue = "", rows = 4, tooltip = '') { const wrapper = document.createElement('div'); wrapper.className = "imgui-control imgui-wrapper"; wrapper.style.cssText = ` display: flex; flex-direction: column; margin-bottom: var(--imgui-bottom-padding); `; const textarea = document.createElement('textarea'); textarea.id = ImmediateGUI.GenerateId("ctrl_"); textarea.placeholder = placeholder; textarea.value = defaultValue; textarea.rows = rows; textarea.className = "imgui-input imgui-control"; textarea.style.cssText = ` background: var(--imgui-input-bg); resize: vertical; min-height: ${rows * 20}px; font-family: inherit; margin-bottom: 0; max-height: calc(${this.maxHeight} - 50px); /* Limit max height to prevent overflowing */ `; if (tooltip && typeof tooltip === 'string' && tooltip.length > 0) textarea.title = tooltip; textarea.addEventListener('mouseup', () => { const container = this.container; const containerMaxHeight = parseInt(getComputedStyle(container).maxHeight); const containerRect = container.getBoundingClientRect(); const textareaRect = textarea.getBoundingClientRect(); // Calculate how much space is available in the container const availableSpace = containerMaxHeight - (textareaRect.top - containerRect.top) - 20; // 20px buffer // If textarea is too tall, limit its height if (textarea.offsetHeight > availableSpace) { textarea.style.height = `${availableSpace}px`; } }); wrapper.appendChild(textarea); this._applyIndentation(wrapper); this._getTargetContainer().appendChild(wrapper); return textarea; } Label(text) { const wrapper = document.createElement('div'); wrapper.className = "imgui-control imgui-wrapper"; wrapper.style.cssText = ` display: flex; margin-bottom: var(--imgui-bottom-padding); `; const label = document.createElement('label'); label.id = ImmediateGUI.GenerateId("ctrl_"); label.textContent = text; label.className = "imgui-label imgui-control"; wrapper.appendChild(label); this._applyIndentation(wrapper); this._getTargetContainer().appendChild(wrapper); return label; } ProgressBar(value = 0, min = 0, max = 100, showText = true, tooltip = '') { // Create wrapper element const wrapper = document.createElement('div'); wrapper.className = "imgui-control imgui-wrapper"; wrapper.style.cssText = ` display: flex; flex-direction: column; width: 100%; margin-bottom: var(--imgui-bottom-padding); `; // Create progress container const progressContainer = document.createElement('div'); progressContainer.style.cssText = ` width: 100%; height: 20px; background: var(--imgui-input-bg); border: 1px solid var(--imgui-border); border-radius: 4px; overflow: hidden; position: relative; `; // Create progress bar element const progressBar = document.createElement('div'); progressBar.id = ImmediateGUI.GenerateId("ctrl_"); progressBar.className = "imgui-progressbar imgui-control"; if (tooltip && typeof tooltip === 'string' && tooltip.length > 0) progressBar.title = tooltip; // Calculate the percentage const normalizedValue = Math.min(Math.max(value, min), max); const percentage = ((normalizedValue - min) / (max - min)) * 100; progressBar.style.cssText = ` width: ${percentage}%; height: 100%; background: var(--imgui-accent); transition: width 0.3s ease; `; // Optional text display let textElement = null; if (showText) { textElement = document.createElement('div'); textElement.textContent = `${Math.round(percentage)}%`; textElement.style.cssText = ` position: absolute; top: 0; left: 0; right: 0; bottom: 0; display: flex; align-items: center; justify-content: center; color: var(--imgui-text); font-size: 12px; font-weight: 500; text-shadow: 0 0 2px rgba(0,0,0,0.5); pointer-events: none; `; progressContainer.appendChild(textElement); } // Add elements to the DOM progressContainer.appendChild(progressBar); wrapper.appendChild(progressContainer); // Add methods to update the progress bar progressBar.setValue = (newValue) => { const normalizedNewValue = Math.min(Math.max(newValue, min), max); const newPercentage = ((normalizedNewValue - min) / (max - min)) * 100; progressBar.style.width = `${newPercentage}%`; if (textElement) { textElement.textContent = `${Math.round(newPercentage)}%`; } }; // Store references progressBar.textElement = textElement; progressBar.min = min; progressBar.max = max; this._applyIndentation(wrapper); this._getTargetContainer().appendChild(wrapper); return progressBar; } ColorPicker(defaultValue = '#000000', tooltip = '') { const wrapper = document.createElement('div'); wrapper.className = "imgui-control imgui-wrapper"; wrapper.style.cssText = `display: flex; align-items: center; padding-bottom: var(--imgui-bottom-padding);`; const colorPicker = document.createElement('input'); colorPicker.id = ImmediateGUI.GenerateId("ctrl_"); colorPicker.type = 'color'; colorPicker.value = defaultValue; colorPicker.className = "imgui-input"; colorPicker.style.cssText = ` margin-right: 8px; width: 80px; height: 40px; border: 1px solid var(--imgui-border); border-radius: 4px; background: none; background-color: var(--imgui-input-bg); cursor: pointer; `; if (tooltip && typeof tooltip === 'string' && tooltip.length > 0) colorPicker.title = tooltip; const colorValue = document.createElement('span'); colorValue.textContent = defaultValue; colorValue.style.cssText = ` font-family: monospace; color: var(--imgui-text); cursor: pointer; `; //colorValue.addEventListener('click', () => { alert('lel'); }); colorPicker.addEventListener('input', () => { colorValue.textContent = colorPicker.value; }); wrapper.appendChild(colorPicker); wrapper.appendChild(colorValue); this._applyIndentation(wrapper); this._getTargetContainer().appendChild(wrapper); return colorPicker; } DatePicker(defaultValue = new Date().toISOString().split('T')[0], tooltip = '') { const wrapper = document.createElement('div'); wrapper.className = "imgui-control imgui-wrapper"; wrapper.style.cssText = ` display: flex; flex-direction: column; margin-bottom: var(--imgui-bottom-padding); `; const datePicker = document.createElement('input'); datePicker.id = ImmediateGUI.GenerateId("ctrl_"); datePicker.type = 'date'; datePicker.value = defaultValue; datePicker.className = "imgui-input imgui-control"; datePicker.style.cursor = "pointer"; if (tooltip && typeof tooltip === 'string' && tooltip.length > 0) datePicker.title = tooltip; wrapper.appendChild(datePicker); this._applyIndentation(wrapper); this._getTargetContainer().appendChild(wrapper); return datePicker; } Dropdown(options = [], defaultValue = null, tooltip = '') { const wrapper = document.createElement('div'); wrapper.className = "imgui-control imgui-wrapper"; wrapper.style.cssText = ` display: flex; flex-direction: column; margin-bottom: var(--imgui-bottom-padding); `; const select = document.createElement('select'); select.id = ImmediateGUI.GenerateId("ctrl_"); select.className = "imgui-input imgui-dropdown imgui-control"; select.style.cssText = ` padding: 6px 10px; border: 1px solid var(--imgui-border); border-radius: 4px; background: var(--imgui-input-bg); color: var(--imgui-text); font-family: inherit; cursor: pointer; appearance: auto; `; if (tooltip && typeof tooltip === 'string' && tooltip.length > 0) select.title = tooltip; // Add options to the select element options.forEach(option => { const optElement = document.createElement('option'); optElement.style.backgroundColor = 'var(--imgui-input-bg)'; optElement.style.color = 'var(--imgui-text)'; // Handle both simple strings and {text, value} objects if (typeof option === 'object' && option !== null) { optElement.textContent = option.text || option.label || ''; optElement.value = option.value !== undefined ? option.value : option.text || ''; } else { optElement.textContent = option; optElement.value = option; } // Set as selected if it matches the default value if (defaultValue !== null && optElement.value === defaultValue) { optElement.selected = true; } select.appendChild(optElement); }); wrapper.appendChild(select); this._applyIndentation(wrapper); this._getTargetContainer().appendChild(wrapper); return select; } NumberInput(label, defaultValue = 0, min = null, max = null, tooltip = '') { const wrapper = document.createElement('div'); wrapper.className = "imgui-control imgui-wrapper"; wrapper.style.cssText = `display: flex; align-items: center; margin-bottom: var(--imgui-bottom-padding);`; const labelElem = document.createElement('label'); labelElem.textContent = label; labelElem.style.cssText = ` margin-right: 10px; margin-top:8px; flex: 1; color: var(--imgui-text); `; const input = document.createElement('input'); input.label = labelElem; input.id = ImmediateGUI.GenerateId("ctrl_"); input.type = 'number'; input.value = defaultValue; if (tooltip && typeof tooltip === 'string' && tooltip.length > 0) input.title = tooltip; if (min !== null) input.min = min; if (max !== null) input.max = max; // TODO: hacky solution to make input elements respect .min and .max values when inputting values manually using the keyboard if (min !== null || max !== null) { input.addEventListener('keyup', () => { let currentValue = parseInt(input.value); if (isNaN(currentValue)) { input.value = Math.floor(min); } else { if (min !== null && currentValue < min) { input.value = Math.floor(min); } else if (max !== null && currentValue > max) { input.value = Math.floor(max); } } }); } input.className = 'imgui-input' input.style.cssText = ` width: 80px; padding: 6px; border: 1px solid var(--imgui-border); border-radius: 4px; background: var(--imgui-input-bg); color: var(--imgui-text); font-family: inherit; `; wrapper.appendChild(labelElem); wrapper.appendChild(input); this._applyIndentation(labelElem); this._getTargetContainer().appendChild(wrapper); return input; } Slider(minValue = 0, maxValue = 100, defaultValue = 50, tooltip = '') { const wrapper = document.createElement('div'); wrapper.className = "imgui-control imgui-wrapper"; wrapper.style.cssText = `display: flex; flex-direction: column; margin-bottom: var(--imgui-bottom-padding);`; const sliderContainer = document.createElement('div'); sliderContainer.style.cssText = `display: flex; align-items: center; width: 100%;`; const slider = document.createElement('input'); slider.className = "imgui-slider"; slider.id = ImmediateGUI.GenerateId("ctrl_"); slider.type = 'range'; slider.min = minValue; slider.max = maxValue; slider.value = defaultValue; slider.style.cssText = ` flex: 1; margin-right: 8px; /* accent-color: var(--imgui-accent); */ `; if (tooltip && typeof tooltip === 'string' && tooltip.length > 0) slider.title = tooltip; const valueDisplay = document.createElement('span'); valueDisplay.textContent = defaultValue; valueDisplay.style.cssText = ` min-width: 40px; text-align: right; color: var(--imgui-text); font-family: inherit; font-weight: 500; `; slider.addEventListener('input', () => { valueDisplay.textContent = slider.value; }); slider.label = valueDisplay; sliderContainer.appendChild(slider); sliderContainer.appendChild(valueDisplay); wrapper.appendChild(sliderContainer); this._applyIndentation(wrapper); this._getTargetContainer().appendChild(wrapper); return slider; } Checkbox(label, checked = false, tooltip = '') { const wrapper = document.createElement('div'); wrapper.className = "imgui-control imgui-wrapper"; wrapper.style.cssText = `display: flex; align-items: center; margin-bottom: var(--imgui-bottom-padding)`; const checkbox = document.createElement('input'); checkbox.id = ImmediateGUI.GenerateId("ctrl_"); checkbox.type = 'checkbox'; checkbox.checked = checked; checkbox.style.cssText = ` margin-right: 8px; accent-color: var(--imgui-accent); clip-path: circle(46% at 50% 50%); width: 16px; height: 16px; `; if (tooltip && typeof tooltip === 'string' && tooltip.length > 0) wrapper.title = tooltip; const labelElem = document.createElement('label'); labelElem.textContent = label; labelElem.htmlFor = checkbox.id; labelElem.style.cssText = ` cursor: pointer; color: var(--imgui-text); font-family: inherit; margin-top: 6px; `; checkbox.label = labelElem; wrapper.appendChild(checkbox); wrapper.appendChild(labelElem); this._applyIndentation(wrapper); this._getTargetContainer().appendChild(wrapper); return checkbox; } RadioButtons(options = [], defaultValue = null, tooltip = '') { const wrapper = document.createElement('div'); wrapper.className = "imgui-control imgui-wrapper"; wrapper.style.cssText = ` display: flex; flex-direction: column; margin-bottom: var(--imgui-bottom-padding); `; if (tooltip && typeof tooltip === 'string' && tooltip.length > 0) wrapper.title = tooltip; // Generate a unique group name for this set of radio buttons const groupName = ImmediateGUI.GenerateId("radio_group_"); // Create an object to store references to the radio buttons const radioButtons = {}; // Add radio buttons to the wrapper options.forEach(option => { // Handle both simple strings and {text, value} objects let text, value; if (typeof option === 'object' && option !== null) { text = option.text || option.label || ''; value = option.value !== undefined ? option.value : option.text || ''; } else { text = option; value = option; } const radioContainer = document.createElement('div'); radioContainer.style.cssText = ` display: flex; align-items: center; `; const radio = document.createElement('input'); radio.id = ImmediateGUI.GenerateId("ctrl_"); radio.type = 'radio'; radio.name = groupName; radio.value = value; radio.checked = value === defaultValue; radio.style.cssText = ` margin-right: 8px; accent-color: var(--imgui-accent); width: 16px; height: 16px; `; const label = document.createElement('label'); label.textContent = text; label.htmlFor = radio.id; label.style.cssText = ` cursor: pointer; color: var(--imgui-text); font-family: inherit; margin-top: ${radioContainer.style.marginBottom || '6px'}; `; radio.label = label; radioContainer.appendChild(radio); radioContainer.appendChild(label); wrapper.appendChild(radioContainer); // Store reference to the radio button radioButtons[value] = radio; }); // Add helper methods to the radio group radioButtons.getChecked = () => { const selected = wrapper.querySelector(`input[name="${groupName}"]:checked`); return selected ? selected.value : null; }; radioButtons.setChecked = (value, checked) => { const radio = wrapper.querySelector(`input[name="${groupName}"][value="${value}"]`); if (radio) { radio.checked = checked; } }; this._applyIndentation(wrapper); this._getTargetContainer().appendChild(wrapper); return radioButtons; } Show() { if (this.container.style.display === 'none') { this.container.style.display = 'block'; return this; } else { if (this.indentationLevel > 0) { console.warn("ImmediateGUI: Show() called while in indentation mode. Did you forget to call EndIndentation() somewhere?"); } if (this.currentSection) { console.warn("ImmediateGUI: Show() called while in section mode. Did you forget to call EndSection() somewhere?"); } if (this.container.children.length === 0) return this; if (!document.body.contains(this.container)) { // Last control wrapper does not need bottom margin, strip that away this.GetControls()[this.GetControls().length - 1].style.marginBottom = '0px'; document.body.appendChild(this.container); } return this; } } Remove() { this.container.remove(); } Hide() { this.container.style.display = "none"; return this; } ShowModal(message, title = '', options = {}) { // Default options const config = { title: title || '', type: 'info', // 'info', 'warning', 'error', 'success' buttons: ['OK'], closeOnBackdropClick: true, width: 400, ...options }; const backdrop = document.createElement('div'); backdrop.className = 'imgui-modal-backdrop'; backdrop.style.cssText = ` position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); z-index: 10000; display: flex; align-items: center; justify-content: center; opacity: 0; transition: opacity 0.2s ease; `; const modal = document.createElement('div'); modal.className = 'imgui-modal'; modal.style.cssText = ` background: var(--imgui-bg); border: 1px solid var(--imgui-border); border-radius: 6px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); width: ${config.width}px; max-width: 90vw; max-height: 80vh; overflow-y: auto; padding: 16px; transform: translateY(-20px); opacity: 0; transition: transform 0.3s ease, opacity 0.3s ease; font-family: inherit; `; if (config.title) { const title = document.createElement('div'); title.className = 'imgui-modal-title'; title.textContent = config.title; title.style.cssText = ` font-size: 18px; font-weight: bold; color: var(--imgui-text); margin-bottom: 12px; padding-bottom: 8px; border-bottom: 1px solid var(--imgui-border); `; modal.appendChild(title); } let iconHtml = ''; if (config.type === 'warning') { iconHtml = '<div style="color: #f0ad4e; margin-right: 10px; font-size: 24px;">⚠️</div>'; } else if (config.type === 'error') { iconHtml = '<div style="color: #d9534f; margin-right: 10px; font-size: 24px;">❌</div>'; } else if (config.type === 'info') { iconHtml = '<div style="color: #5bc0de; margin-right: 10px; font-size: 24px;">ℹ️</div>'; } else if (config.type === 'success') { iconHtml = '<div style="color: #5cb85c; margin-right: 10px; font-size: 24px;">✅</div>'; } const messageContainer = document.createElement('div'); messageContainer.className = 'imgui-modal-message'; messageContainer.style.cssText = ` color: var(--imgui-text); margin-bottom: 16px; line-height: 1.5; display: flex; align-items: flex-start; `; if (iconHtml) { const iconElement = document.createElement('div'); iconElement.innerHTML = iconHtml; messageContainer.appendChild(iconElement); } const messageText = document.createElement('div'); messageText.style.flex = '1'; if (typeof message === 'object' && message.nodeType) { messageText.appendChild(message); } else { messageText.textContent = message; } messageContainer.appendChild(messageText); modal.appendChild(messageContainer); const buttonsContainer = document.createElement('div'); buttonsContainer.className = 'imgui-modal-buttons'; buttonsContainer.style.cssText = ` display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; `; const closeModal = () => { modal.style.transform = 'translateY(-20px)'; modal.style.opacity = '0'; backdrop.style.opacity = '0'; setTimeout(() => { document.body.removeChild(backdrop); }, 300); }; const buttonsList = Array.isArray(config.buttons) ? config.buttons : [config.buttons]; buttonsList.forEach((buttonConfig) => { const isObject = typeof buttonConfig === 'object'; const buttonText = isObject ? buttonConfig.text : buttonConfig; const isPrimary = isObject ? buttonConfig.primary : false; const callback = isObject ? buttonConfig.callback : null; const button = document.createElement('button'); button.textContent = buttonText; button.className = isPrimary ? 'imgui-button imgui-primary-button' : 'imgui-button'; if (isPrimary) { button.style.background = 'var(--imgui-accent)'; button.style.color = '#ffffff'; button.style.borderColor = 'var(--imgui-accent)'; } button.addEventListener('click', () => { if (callback) callback(); closeModal(); }); buttonsContainer.appendChild(button); }); modal.appendChild(buttonsContainer); backdrop.appendChild(modal); if (config.closeOnBackdropClick) { backdrop.addEventListener('click', (e) => { if (e.target === backdrop) { closeModal(); } }); } const escHandler = (e) => { if (e.key === 'Escape') { closeModal(); document.removeEventListener('keydown', escHandler); } }; document.addEventListener('keydown', escHandler); document.body.appendChild(backdrop); setTimeout(() => { backdrop.style.opacity = '1'; modal.style.transform = 'translateY(0)'; modal.style.opacity = '1'; }, 10); return { close: closeModal, element: modal, backdrop: backdrop }; } // New methods for better theming and layout SetTheme(themeName) { if (this.themes[themeName]) { this.options.theme = themeName; this.theme = this.themes[themeName]; this._updateCSSVariables(); this._applyThemeToElements(); } return this; } _applyThemeToElements() { // Update container this.container.style.background = this.theme.background; this.container.style.color = this.theme.text; this.container.style.borderColor = this.theme.border; // Update all controls this.container.querySelectorAll('.imgui-button').forEach(el => { el.style.background = this.theme.buttonBg; el.style.color = this.theme.text; el.style.borderColor = this.theme.border; }); this.container.querySelectorAll('.imgui-input').forEach(el => { el.style.background = this.theme.inputBg; el.style.color = this.theme.text; el.style.borderColor = this.theme.border; }); this.container.querySelectorAll('.imgui-section').forEach(el => { // Shitty hack to make the section header text color change for sections // that are not collapsible el.querySelectorAll('.imgui-section-header').forEach(h => { h.style.color = this.theme.text;}); el.style.background = this.theme.sectionBg; el.style.borderColor = this.theme.border; }); // this.container.querySelectorAll('.imgui-progressbar').forEach(el => { // // el.style.background = this.theme.inputBg; // // el.style.borderColor = this.theme.border; // }); // Update text elements this.container.querySelectorAll('label, h1, h2, h3, h4, h5, h6, span').forEach(el => { el.style.color = this.theme.text; }); return this; } }