extends PanelContainer const Style := preload("res://scripts/ui/ui_style.gd") const Brand := preload("res://scripts/ui/medmind_style.gd") const MAX_ENTRY_LENGTH := 16 var display_label: Label var operation_label: Label var keypad_buttons: Array[Button] = [] var entry := "0" var accumulator := 0.0 var pending_operator := "" var waiting_for_operand := true var last_operator := "" var last_operand := 0.0 var has_last_operation := false var error_state := false var drag_active := false var drag_offset := Vector2.ZERO var has_been_positioned := false func _ready() -> void: name = "CalculatorPanel" visible = false top_level = true mouse_filter = Control.MOUSE_FILTER_STOP custom_minimum_size = Vector2(310, 390) size = custom_minimum_size _build_ui() if not get_viewport().size_changed.is_connected(_on_viewport_size_changed): get_viewport().size_changed.connect(_on_viewport_size_changed) _on_viewport_size_changed() refresh_theme_controls() _refresh_display() func open_panel() -> void: visible = true move_to_front() if not has_been_positioned: var viewport_size := get_viewport_rect().size position = Vector2(maxf(12.0, viewport_size.x - size.x - 24.0), 72.0) has_been_positioned = true _clamp_to_viewport() func close_panel() -> void: visible = false drag_active = false func toggle_panel() -> void: if visible: close_panel() else: open_panel() func reset_calculation() -> void: entry = "0" accumulator = 0.0 pending_operator = "" waiting_for_operand = true last_operator = "" last_operand = 0.0 has_last_operation = false error_state = false _refresh_display() func press_token(token: String) -> void: if token.length() == 1 and token.is_valid_int(): _enter_digit(token) return match token: ".": _enter_decimal() "+", "−", "×", "÷": _set_operator(token) "=": _equals() "%": _apply_unary("percent") "√": _apply_unary("sqrt") "±": _apply_unary("sign") "⌫": _backspace() "AC": reset_calculation() func get_display_text() -> String: return entry func refresh_theme_controls() -> void: if display_label == null: return var dark := UIThemeService.is_dark_mode() add_theme_stylebox_override("panel", Brand.content_card_box(dark, true, Brand.ROYAL_BLUE)) display_label.add_theme_color_override("font_color", Brand.text_color(dark)) operation_label.add_theme_color_override("font_color", Brand.muted_color(dark)) for button in keypad_buttons: var accent := Brand.ROYAL_BLUE if str(button.get_meta("calculator_token", "")) in ["+", "−", "×", "÷", "="] else Brand.SLATE button.add_theme_stylebox_override("normal", Brand.badge_box(dark, accent)) button.add_theme_stylebox_override("hover", Brand.badge_box(dark, Brand.TEAL)) button.add_theme_stylebox_override("pressed", Brand.badge_box(dark, Brand.PURPLE)) button.add_theme_color_override("font_color", Brand.text_color(dark)) func _build_ui() -> void: var margin := MarginContainer.new() margin.add_theme_constant_override("margin_left", 10) margin.add_theme_constant_override("margin_right", 10) margin.add_theme_constant_override("margin_top", 12) margin.add_theme_constant_override("margin_bottom", 14) add_child(margin) var content := VBoxContainer.new() content.add_theme_constant_override("separation", 10) margin.add_child(content) var title_bar := HBoxContainer.new() title_bar.name = "CalculatorTitleBar" title_bar.mouse_filter = Control.MOUSE_FILTER_STOP title_bar.custom_minimum_size.y = 34 title_bar.gui_input.connect(_on_title_bar_gui_input) content.add_child(title_bar) var title := Label.new() title.text = "Calculator" title.size_flags_horizontal = Control.SIZE_EXPAND_FILL title.mouse_filter = Control.MOUSE_FILTER_IGNORE Brand.apply_font(title) title.add_theme_font_size_override("font_size", 18) title_bar.add_child(title) var close := Button.new() close.name = "CalculatorClose" close.text = "×" close.tooltip_text = "Close calculator" close.custom_minimum_size = Vector2(36, 32) close.focus_mode = Control.FOCUS_NONE close.pressed.connect(close_panel) title_bar.add_child(close) var display_panel := PanelContainer.new() display_panel.custom_minimum_size.y = 78 display_panel.add_theme_stylebox_override("panel", Style.panel_box(Color("101b31"), Style.CYAN)) content.add_child(display_panel) var display_margin := MarginContainer.new() display_margin.add_theme_constant_override("margin_left", 12) display_margin.add_theme_constant_override("margin_right", 12) display_margin.add_theme_constant_override("margin_top", 8) display_margin.add_theme_constant_override("margin_bottom", 8) display_panel.add_child(display_margin) var display_stack := VBoxContainer.new() display_margin.add_child(display_stack) operation_label = Label.new() operation_label.name = "CalculatorOperation" operation_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT operation_label.add_theme_font_size_override("font_size", 13) Brand.apply_font(operation_label) display_stack.add_child(operation_label) display_label = Label.new() display_label.name = "CalculatorDisplay" display_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT display_label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS display_label.add_theme_font_size_override("font_size", 28) Brand.apply_font(display_label) display_stack.add_child(display_label) var keypad := GridContainer.new() keypad.name = "CalculatorKeypad" keypad.columns = 4 keypad.add_theme_constant_override("h_separation", 6) keypad.add_theme_constant_override("v_separation", 8) content.add_child(keypad) var tokens := [ "AC", "⌫", "±", "√", "7", "8", "9", "÷", "4", "5", "6", "×", "1", "2", "3", "−", "%", "0", ".", "+", "", "", "", "=" ] for token in tokens: var button := Button.new() button.custom_minimum_size = Vector2(44, 45) button.size_flags_horizontal = Control.SIZE_EXPAND_FILL button.focus_mode = Control.FOCUS_NONE if token == "": button.visible = false keypad.add_child(button) continue button.text = token button.set_meta("calculator_token", token) button.tooltip_text = _token_tooltip(token) button.add_theme_font_size_override("font_size", 18) Brand.apply_font(button) UISoundService.set_button_sound(button, "toggle") button.pressed.connect(press_token.bind(token)) keypad.add_child(button) keypad_buttons.append(button) func _input(event: InputEvent) -> void: if not visible or not (event is InputEventKey): return var key_event := event as InputEventKey if not key_event.pressed or key_event.echo: return var token := "" if key_event.keycode == KEY_ESCAPE: close_panel() get_viewport().set_input_as_handled() return if key_event.keycode in [KEY_ENTER, KEY_KP_ENTER]: token = "=" elif key_event.keycode == KEY_BACKSPACE: token = "⌫" elif key_event.keycode == KEY_DELETE: token = "AC" elif key_event.unicode > 0: var character := String.chr(key_event.unicode) match character: "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ".", "%", "=": token = character "+": token = "+" "-": token = "−" "*", "x", "X": token = "×" "/": token = "÷" "c", "C": token = "AC" if token == "": return press_token(token) get_viewport().set_input_as_handled() func _enter_digit(digit: String) -> void: if error_state: reset_calculation() if waiting_for_operand: entry = digit waiting_for_operand = false elif entry == "0": entry = digit elif entry.length() < MAX_ENTRY_LENGTH: entry += digit _refresh_display() func _enter_decimal() -> void: if error_state: reset_calculation() if waiting_for_operand: entry = "0." waiting_for_operand = false elif not "." in entry and entry.length() < MAX_ENTRY_LENGTH: entry += "." _refresh_display() func _set_operator(operator: String) -> void: if error_state: return var current := _entry_value() if pending_operator != "" and not waiting_for_operand: if not _perform_operation(pending_operator, current): return else: accumulator = current pending_operator = operator waiting_for_operand = true has_last_operation = false _refresh_display() func _equals() -> void: if error_state: return if pending_operator != "": var operand := accumulator if waiting_for_operand else _entry_value() var operator := pending_operator if not _perform_operation(operator, operand): return last_operator = operator last_operand = operand has_last_operation = true pending_operator = "" elif has_last_operation: if not _perform_operation(last_operator, last_operand): return else: return waiting_for_operand = true _refresh_display() func _apply_unary(operation: String) -> void: if error_state: return var value := _entry_value() match operation: "percent": value /= 100.0 "sqrt": if value < 0.0: _set_error() return value = sqrt(value) "sign": value = -value entry = _format_number(value) if pending_operator == "": accumulator = value waiting_for_operand = false _refresh_display() func _backspace() -> void: if error_state: reset_calculation() return if waiting_for_operand: return entry = entry.left(entry.length() - 1) if entry in ["", "-"]: entry = "0" waiting_for_operand = true _refresh_display() func _perform_operation(operator: String, operand: float) -> bool: var result := accumulator match operator: "+": result += operand "−": result -= operand "×": result *= operand "÷": if is_zero_approx(operand): _set_error() return false result /= operand if is_nan(result) or is_inf(result): _set_error() return false accumulator = result entry = _format_number(result) return true func _entry_value() -> float: return entry.to_float() func _format_number(value: float) -> String: if is_zero_approx(value): return "0" var formatted := String.num(value, 12) if "." in formatted: while formatted.ends_with("0"): formatted = formatted.left(formatted.length() - 1) if formatted.ends_with("."): formatted = formatted.left(formatted.length() - 1) if formatted.length() > MAX_ENTRY_LENGTH: formatted = String.num_scientific(value).replace("e+", "e") return formatted func _set_error() -> void: entry = "Error" error_state = true pending_operator = "" waiting_for_operand = true has_last_operation = false _refresh_display() func _refresh_display() -> void: if display_label == null: return display_label.text = entry operation_label.text = ("%s %s" % [_format_number(accumulator), pending_operator]) if pending_operator != "" else "" func _on_title_bar_gui_input(event: InputEvent) -> void: var touch_event := event as InputEventScreenTouch if touch_event != null: drag_active = touch_event.pressed if drag_active: drag_offset = touch_event.position get_viewport().set_input_as_handled() return var touch_drag := event as InputEventScreenDrag if touch_drag != null and drag_active: global_position += touch_drag.relative _clamp_to_viewport() get_viewport().set_input_as_handled() return var mouse_event := event as InputEventMouseButton if mouse_event != null and mouse_event.button_index == MOUSE_BUTTON_LEFT: drag_active = mouse_event.pressed if drag_active: drag_offset = mouse_event.global_position - global_position get_viewport().set_input_as_handled() return var motion := event as InputEventMouseMotion if motion != null and drag_active: global_position = motion.global_position - drag_offset _clamp_to_viewport() get_viewport().set_input_as_handled() func _on_viewport_size_changed() -> void: var viewport_size := get_viewport_rect().size custom_minimum_size.x = minf(310.0, maxf(216.0, viewport_size.x - 24.0)) size.x = custom_minimum_size.x _clamp_to_viewport() func _clamp_to_viewport() -> void: if not is_inside_tree(): return var viewport_size := get_viewport_rect().size position.x = clampf(position.x, 12.0, maxf(12.0, viewport_size.x - size.x - 12.0)) position.y = clampf(position.y, 12.0, maxf(12.0, viewport_size.y - size.y - 12.0)) func _token_tooltip(token: String) -> String: return { "AC": "Clear calculation", "⌫": "Backspace", "±": "Change sign", "√": "Square root", "%": "Convert to percent", "÷": "Divide", "×": "Multiply", "−": "Subtract", "+": "Add", "=": "Equals" }.get(token, token)