├── README.md ├── clear.png ├── Info.plist ├── Makefile ├── create_icon.sh └── clear.cc /README.md: -------------------------------------------------------------------------------- 1 | # Clear-txt 2 | 3 | ![](clear.png) 4 | -------------------------------------------------------------------------------- /clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oxUnd/Clear-txt/main/clear.png -------------------------------------------------------------------------------- /Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | Clear-txt 9 | CFBundleIdentifier 10 | com.oxund.clear-txt 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | Clear-txt 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 2 21 | LSMinimumSystemVersion 22 | 12.0 23 | NSHighResolutionCapable 24 | 25 | NSPrincipalClass 26 | NSApplication 27 | CFBundleIconFile 28 | Clear.icns 29 | CFBundleIconFiles 30 | 31 | Clear.icns 32 | 33 | LSApplicationCategoryType 34 | public.app-category.productivity 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CXX = g++ 2 | CXXFLAGS = -Wall -std=c++11 3 | 4 | # Use fltk-config to get FLTK flags 5 | FLTK_CXXFLAGS = `fltk-config --cxxflags` 6 | FLTK_LDFLAGS = `fltk-config --ldflags` 7 | FLTK_LDSTATICFLAGS = `fltk-config --ldstaticflags` 8 | 9 | TARGET = clear 10 | TARGET_STATIC = clear-static 11 | SOURCE = clear.cc 12 | 13 | all: $(TARGET) 14 | 15 | static: $(TARGET_STATIC) 16 | 17 | $(TARGET): $(SOURCE) 18 | $(CXX) $(CXXFLAGS) $(FLTK_CXXFLAGS) -o $(TARGET) $(SOURCE) $(FLTK_LDFLAGS) 19 | 20 | # Static build - links FLTK statically 21 | $(TARGET_STATIC): $(SOURCE) 22 | @echo "Building static version..." 23 | $(CXX) $(CXXFLAGS) $(FLTK_CXXFLAGS) -o $(TARGET_STATIC) $(SOURCE) $(FLTK_LDSTATICFLAGS) 24 | 25 | icon: Clear.icns 26 | 27 | Clear.icns: 28 | @if [ ! -f create_icon.sh ]; then \ 29 | echo "Error: create_icon.sh not found"; \ 30 | exit 1; \ 31 | fi 32 | @./create_icon.sh 33 | 34 | app: $(TARGET_STATIC) Clear.icns 35 | @echo "Creating macOS .app bundle..." 36 | @rm -rf Clear-txt.app 37 | @mkdir -p Clear-txt.app/Contents/MacOS 38 | @mkdir -p Clear-txt.app/Contents/Resources 39 | @cp $(TARGET_STATIC) Clear-txt.app/Contents/MacOS/Clear-txt 40 | @cp Info.plist Clear-txt.app/Contents/Info.plist 41 | @cp Clear.icns Clear-txt.app/Contents/Resources/ 42 | @chmod +x Clear-txt.app/Contents/MacOS/Clear-txt 43 | @echo "Clear.app bundle created successfully!" 44 | 45 | clean: 46 | rm -f $(TARGET) $(TARGET_STATIC) 47 | rm -rf Clear-txt.app 48 | rm -f Clear.icns 49 | 50 | .PHONY: all static app icon clean 51 | 52 | -------------------------------------------------------------------------------- /create_icon.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Create icon for Clear app using ImageMagick 3 | # Generate .icns file for macOS 4 | 5 | set -e 6 | 7 | ICONSET_DIR="Clear.iconset" 8 | rm -rf "$ICONSET_DIR" 9 | mkdir -p "$ICONSET_DIR" 10 | 11 | # Create base 1024x1024 icon following Apple HIG guidelines 12 | # Background must fill entire canvas (1024x1024) - no transparency 13 | # Main content (checkmark) should be in safe area (10-20% margin from edges) 14 | # According to Apple: https://developer.apple.com/design/human-interface-guidelines/app-icons 15 | TEMP_ICON="$ICONSET_DIR/icon_1024.png" 16 | magick -size 1024x1024 \ 17 | gradient:"rgb(255,70,70)-rgb(255,220,60)" \ 18 | -alpha off \ 19 | -fill white \ 20 | -stroke white \ 21 | -strokewidth 200 \ 22 | -draw "path 'M 250,450 L 512,750 L 800,250'" \ 23 | -alpha off \ 24 | "$TEMP_ICON" 25 | 26 | # Generate all required sizes for iconset 27 | echo "Generating icon sizes..." 28 | 29 | # 16x16 30 | magick "$TEMP_ICON" -resize 16x16 "$ICONSET_DIR/icon_16x16.png" 31 | 32 | # 16x16@2x (32x32) 33 | magick "$TEMP_ICON" -resize 32x32 "$ICONSET_DIR/icon_16x16@2x.png" 34 | 35 | # 32x32 36 | magick "$TEMP_ICON" -resize 32x32 "$ICONSET_DIR/icon_32x32.png" 37 | 38 | # 32x32@2x (64x64) 39 | magick "$TEMP_ICON" -resize 64x64 "$ICONSET_DIR/icon_32x32@2x.png" 40 | 41 | # 128x128 42 | magick "$TEMP_ICON" -resize 128x128 "$ICONSET_DIR/icon_128x128.png" 43 | 44 | # 128x128@2x (256x256) 45 | magick "$TEMP_ICON" -resize 256x256 "$ICONSET_DIR/icon_128x128@2x.png" 46 | 47 | # 256x256 48 | magick "$TEMP_ICON" -resize 256x256 "$ICONSET_DIR/icon_256x256.png" 49 | 50 | # 256x256@2x (512x512) 51 | magick "$TEMP_ICON" -resize 512x512 "$ICONSET_DIR/icon_256x256@2x.png" 52 | 53 | # 512x512 54 | magick "$TEMP_ICON" -resize 512x512 "$ICONSET_DIR/icon_512x512.png" 55 | 56 | # 512x512@2x (1024x1024) 57 | cp "$TEMP_ICON" "$ICONSET_DIR/icon_512x512@2x.png" 58 | 59 | # Remove temporary 1024x1024 file 60 | rm -f "$TEMP_ICON" 61 | 62 | # Convert iconset to .icns 63 | echo "Converting to .icns format..." 64 | iconutil -c icns "$ICONSET_DIR" -o "Clear.icns" 65 | 66 | # Clean up temporary iconset directory 67 | rm -rf "$ICONSET_DIR" 68 | 69 | echo "Icon file created: Clear.icns" 70 | echo "✓ Icon generation complete" 71 | 72 | -------------------------------------------------------------------------------- /clear.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #ifdef _WIN32 16 | #include 17 | #include 18 | #else 19 | #include 20 | #endif 21 | 22 | struct TodoItem { 23 | std::string text; 24 | bool completed; 25 | int y_position; 26 | int swipe_offset; // Horizontal offset for swipe gesture (positive = right, 27 | // negative = left) 28 | 29 | TodoItem(const std::string &t) 30 | : text(t), completed(false), y_position(0), swipe_offset(0) {} 31 | }; 32 | 33 | class ClearApp : public Fl_Window { 34 | private: 35 | std::vector items; 36 | int selected_index; 37 | int drag_start_y; 38 | int drag_start_x; 39 | bool is_dragging; 40 | bool is_swiping; 41 | bool is_pulling_down; // Pull down to add new item 42 | int pull_down_offset; // Offset for pull-down animation 43 | int drag_offset; 44 | int item_height; 45 | std::string data_file; 46 | int editing_index; // Index of item being edited 47 | std::string editing_text; // Text being edited 48 | int pending_click_index; // Index of item pending click (to handle 49 | // double-click) 50 | bool can_reorder; // Whether reordering is allowed (after long press) 51 | Fl_Input *input_widget; // Input widget for editing items 52 | int scroll_offset; // Vertical scroll offset (positive = scrolled down) 53 | 54 | // Error message display 55 | struct ErrorDisplay { 56 | std::string message; 57 | bool is_visible; 58 | 59 | ErrorDisplay() : message(""), is_visible(false) {} 60 | } error_display; 61 | 62 | // Calculate luminance of a color (0-255) 63 | double get_luminance(Fl_Color color) { 64 | unsigned char r, g, b; 65 | Fl::get_color(color, r, g, b); 66 | // Use relative luminance formula (ITU-R BT.709) 67 | return 0.2126 * r + 0.7152 * g + 0.0722 * b; 68 | } 69 | 70 | // Get appropriate text color based on background color 71 | Fl_Color get_text_color(Fl_Color bg_color) { 72 | double luminance = get_luminance(bg_color); 73 | // If background is bright (luminance > 128), use black text 74 | // Otherwise use white text 75 | return (luminance > 128) ? FL_BLACK : FL_WHITE; 76 | } 77 | 78 | // Get color based on position in list (red -> orange -> yellow gradient) 79 | Fl_Color get_color_by_position(int position, int total_items) { 80 | if (total_items <= 1) { 81 | return FL_RED; 82 | } 83 | 84 | // Calculate ratio from 0.0 (top, red) to 1.0 (bottom, yellow) 85 | double ratio = (double)position / (total_items - 1); 86 | 87 | // Interpolate from red (255,0,0) -> orange (255,165,0) -> yellow 88 | // (255,255,0) 89 | unsigned char r, g, b; 90 | 91 | if (ratio < 0.5) { 92 | // Red to Orange (0.0 to 0.5) 93 | double local_ratio = ratio * 2.0; // 0.0 to 1.0 94 | r = 255; 95 | g = (unsigned char)(165 * local_ratio); 96 | b = 0; 97 | } else { 98 | // Orange to Yellow (0.5 to 1.0) 99 | double local_ratio = (ratio - 0.5) * 2.0; // 0.0 to 1.0 100 | r = 255; 101 | g = (unsigned char)(165 + (255 - 165) * local_ratio); 102 | b = 0; 103 | } 104 | 105 | return fl_rgb_color(r, g, b); 106 | } 107 | 108 | // Get application data directory path 109 | static std::string get_data_directory() { 110 | std::string home_dir; 111 | std::string data_dir; 112 | 113 | #ifdef _WIN32 114 | // Windows: Use %APPDATA%\Clear 115 | char appdata_path[MAX_PATH]; 116 | if (SUCCEEDED(SHGetFolderPathA(NULL, CSIDL_APPDATA, NULL, 117 | SHGFP_TYPE_CURRENT, appdata_path))) { 118 | home_dir = appdata_path; 119 | data_dir = home_dir + "\\Clear"; 120 | } else { 121 | // Fallback to current directory 122 | data_dir = "."; 123 | } 124 | #else 125 | // Unix-like systems (macOS, Linux) 126 | const char *home = getenv("HOME"); 127 | if (!home) { 128 | // Fallback to getpwuid 129 | struct passwd *pw = getpwuid(getuid()); 130 | if (pw) { 131 | home = pw->pw_dir; 132 | } 133 | } 134 | 135 | if (home) { 136 | home_dir = home; 137 | #ifdef __APPLE__ 138 | // macOS: ~/Library/Application Support/Clear 139 | data_dir = home_dir + "/Library/Application Support/Clear"; 140 | #else 141 | // Linux: ~/.config/Clear (or ~/.local/share/Clear) 142 | // Using XDG_CONFIG_HOME if set, otherwise ~/.config 143 | const char *xdg_config = getenv("XDG_CONFIG_HOME"); 144 | if (xdg_config) { 145 | data_dir = std::string(xdg_config) + "/Clear"; 146 | } else { 147 | data_dir = home_dir + "/.config/Clear"; 148 | } 149 | #endif 150 | } else { 151 | // Fallback to current directory 152 | data_dir = "."; 153 | } 154 | #endif 155 | 156 | // Create directory if it doesn't exist (recursively) 157 | if (data_dir != ".") { 158 | #ifdef _WIN32 159 | // Create all parent directories recursively on Windows 160 | std::string path = data_dir; 161 | size_t pos = 0; 162 | while ((pos = path.find_first_of("\\/", pos + 1)) != std::string::npos) { 163 | std::string dir = path.substr(0, pos); 164 | CreateDirectoryA(dir.c_str(), NULL); 165 | } 166 | // Create the final directory 167 | CreateDirectoryA(data_dir.c_str(), NULL); 168 | #else 169 | // Create all parent directories recursively 170 | std::string path = data_dir; 171 | size_t pos = 0; 172 | while ((pos = path.find_first_of('/', pos + 1)) != std::string::npos) { 173 | std::string dir = path.substr(0, pos); 174 | mkdir(dir.c_str(), 0755); 175 | } 176 | // Create the final directory 177 | mkdir(data_dir.c_str(), 0755); 178 | #endif 179 | } 180 | 181 | return data_dir; 182 | } 183 | 184 | // Escape/unescape text for file storage 185 | std::string escape_text(const std::string &text) { 186 | std::string result; 187 | for (char c : text) { 188 | if (c == '\n') { 189 | result += "\\n"; 190 | } else if (c == '\\') { 191 | result += "\\\\"; 192 | } else { 193 | result += c; 194 | } 195 | } 196 | return result; 197 | } 198 | 199 | std::string unescape_text(const std::string &text) { 200 | std::string result; 201 | for (size_t i = 0; i < text.length(); i++) { 202 | if (text[i] == '\\' && i + 1 < text.length()) { 203 | if (text[i + 1] == 'n') { 204 | result += '\n'; 205 | i++; 206 | } else if (text[i + 1] == '\\') { 207 | result += '\\'; 208 | i++; 209 | } else { 210 | result += text[i]; 211 | } 212 | } else { 213 | result += text[i]; 214 | } 215 | } 216 | return result; 217 | } 218 | 219 | void show_error(const std::string &message) { 220 | // Cancel any existing timeout to prevent multiple timers 221 | Fl::remove_timeout(hide_error_cb, this); 222 | 223 | error_display.message = message; 224 | error_display.is_visible = true; 225 | 226 | // Auto-hide after 3 seconds 227 | Fl::add_timeout(3.0, hide_error_cb, this); 228 | redraw(); 229 | } 230 | 231 | void hide_error() { 232 | error_display.is_visible = false; 233 | error_display.message = ""; 234 | Fl::remove_timeout(hide_error_cb, this); 235 | redraw(); 236 | } 237 | 238 | static void hide_error_cb(void *data) { 239 | ClearApp *app = static_cast(data); 240 | app->hide_error(); 241 | } 242 | 243 | // Calculate text dimensions more accurately 244 | void measure_text(const std::string &text, int &width, int &height, 245 | int font_size = 14) { 246 | fl_font(FL_HELVETICA_BOLD, font_size); 247 | width = (int)fl_width(text.c_str()); 248 | height = (int)fl_height(); 249 | } 250 | 251 | // Draw rounded rectangle with specified color and corner radius 252 | void draw_rounded_rect(int x, int y, int w, int h, int radius) { 253 | // Draw four rounded corners using pie slices 254 | // Top-left corner 255 | fl_pie(x, y, radius * 2, radius * 2, 90, 180); 256 | // Top-right corner 257 | fl_pie(x + w - radius * 2, y, radius * 2, radius * 2, 0, 90); 258 | // Bottom-right corner 259 | fl_pie(x + w - radius * 2, y + h - radius * 2, radius * 2, radius * 2, 270, 260 | 360); 261 | // Bottom-left corner 262 | fl_pie(x, y + h - radius * 2, radius * 2, radius * 2, 180, 270); 263 | 264 | // Draw rectangular parts (top, middle, bottom) 265 | fl_rectf(x + radius, y, w - radius * 2, h); // Middle vertical strip 266 | fl_rectf(x, y + radius, radius, h - radius * 2); // Left strip 267 | fl_rectf(x + w - radius, y + radius, radius, h - radius * 2); // Right strip 268 | } 269 | 270 | // Draw rounded rectangle border 271 | void draw_rounded_rect_border(int x, int y, int w, int h, int radius) { 272 | // Draw four corner arcs 273 | fl_arc(x, y, radius * 2, radius * 2, 90, 180); // Top-left 274 | fl_arc(x + w - radius * 2, y, radius * 2, radius * 2, 0, 90); // Top-right 275 | fl_arc(x + w - radius * 2, y + h - radius * 2, radius * 2, radius * 2, 270, 276 | 360); // Bottom-right 277 | fl_arc(x, y + h - radius * 2, radius * 2, radius * 2, 180, 278 | 270); // Bottom-left 279 | 280 | // Draw straight edges 281 | fl_line(x + radius, y, x + w - radius, y); // Top 282 | fl_line(x + w, y + radius, x + w, y + h - radius); // Right 283 | fl_line(x + w - radius, y + h, x + radius, y + h); // Bottom 284 | fl_line(x, y + h - radius, x, y + radius); // Left 285 | } 286 | 287 | void save_to_file() { 288 | std::ofstream file(data_file); 289 | if (!file.is_open()) { 290 | show_error("Failed to save file: " + data_file); 291 | return; 292 | } 293 | 294 | for (const auto &item : items) { 295 | // Save with color index 0 for backward compatibility (color is now 296 | // position-based) 297 | file << "0|" << (item.completed ? "1" : "0") << "|" 298 | << escape_text(item.text) << "\n"; 299 | } 300 | 301 | file.close(); 302 | 303 | // Check if write failed 304 | if (file.fail()) { 305 | show_error("Error saving file: " + data_file); 306 | } 307 | } 308 | 309 | bool load_from_file() { 310 | std::ifstream file(data_file); 311 | if (!file.is_open()) { 312 | // File doesn't exist or can't be opened 313 | // This is normal for first run, so don't show error 314 | return false; 315 | } 316 | 317 | items.clear(); 318 | std::string line; 319 | bool loaded_any = false; 320 | while (std::getline(file, line)) { 321 | if (line.empty()) 322 | continue; 323 | 324 | size_t pos1 = line.find('|'); 325 | if (pos1 == std::string::npos) 326 | continue; 327 | 328 | size_t pos2 = line.find('|', pos1 + 1); 329 | if (pos2 == std::string::npos) 330 | continue; 331 | 332 | // Color index is stored but not used (for backward compatibility) 333 | bool completed = (line.substr(pos1 + 1, pos2 - pos1 - 1) == "1"); 334 | std::string text = unescape_text(line.substr(pos2 + 1)); 335 | 336 | TodoItem item(text); 337 | item.completed = completed; 338 | items.push_back(item); 339 | loaded_any = true; 340 | } 341 | 342 | file.close(); 343 | 344 | // Check if read failed 345 | if (file.fail() && !file.eof()) { 346 | show_error("Error reading file: " + data_file); 347 | } 348 | 349 | return loaded_any; // Return true if we loaded at least one item 350 | } 351 | 352 | void add_sample_items() { 353 | // Add sample items for first-time users 354 | items.push_back(TodoItem("Welcome to Clear")); 355 | items.push_back(TodoItem("Pull down to add new task")); 356 | items.push_back(TodoItem("Click to edit task")); 357 | items.push_back(TodoItem("Double-click to complete")); 358 | items.push_back(TodoItem("Swipe right to delete")); 359 | items.push_back(TodoItem("Long press to reorder")); 360 | } 361 | 362 | void draw_item(int index, int y, bool is_editing = false, int visual_position = -1, int total_visual_items = -1) { 363 | if (index < 0 || index >= (int)items.size()) 364 | return; 365 | 366 | TodoItem &item = items[index]; 367 | item.y_position = y; 368 | 369 | int x_offset = item.swipe_offset; 370 | int abs_offset = abs(x_offset); 371 | 372 | // Get color based on position in list (not item's stored color) 373 | // For completed items, use dark gray instead 374 | Fl_Color item_color; 375 | if (item.completed) { 376 | item_color = fl_rgb_color(64, 64, 64); // Dark gray for completed items 377 | } else { 378 | // Use visual position if provided (for sorted display), otherwise use actual index 379 | if (visual_position >= 0 && total_visual_items > 0) { 380 | item_color = get_color_by_position(visual_position, total_visual_items); 381 | } else { 382 | item_color = get_color_by_position(index, items.size()); 383 | } 384 | } 385 | 386 | // Draw swipe background based on direction 387 | if (x_offset > 0) { 388 | // Swiped right (finger moves right) - item moves right, show complete 389 | // background (green) on left 390 | int right_offset = abs_offset; 391 | if (right_offset > w()) 392 | right_offset = w(); 393 | fl_color(FL_GREEN); 394 | fl_rectf(0, y, right_offset, item_height); 395 | fl_color(FL_WHITE); 396 | fl_font(FL_HELVETICA_BOLD, 16); 397 | fl_draw("COMPLETE", right_offset / 2 - 40, y + item_height / 2 + 5); 398 | } else if (x_offset < 0) { 399 | // Swiped left (finger moves left) - item moves left, show delete 400 | // background (red) on right 401 | int left_offset = abs_offset; 402 | if (left_offset > w()) 403 | left_offset = w(); 404 | fl_color(FL_RED); 405 | fl_rectf(w() - left_offset, y, left_offset, item_height); 406 | fl_color(FL_WHITE); 407 | fl_font(FL_HELVETICA_BOLD, 16); 408 | fl_draw("DELETE", w() - left_offset / 2 - 30, y + item_height / 2 + 5); 409 | } 410 | 411 | // Draw colored background (shifted by swipe) 412 | int bg_x = (x_offset > 0) ? abs_offset : 0; 413 | int bg_w = w() - abs_offset; 414 | if (bg_w < 0) 415 | bg_w = 0; 416 | 417 | if (is_editing) { 418 | // When editing, only draw background in the left 20px padding area 419 | // Fl_Input widget will handle the rest 420 | fl_color(item_color); 421 | fl_rectf(bg_x, y, 20, item_height); 422 | } else { 423 | // When not editing, draw full background and text 424 | fl_color(item_color); 425 | fl_rectf(bg_x, y, bg_w, item_height); 426 | 427 | // Draw text with appropriate color based on background 428 | Fl_Color text_color = get_text_color(item_color); 429 | fl_color(text_color); 430 | fl_font(FL_HELVETICA_BOLD, 18); 431 | 432 | std::string display_text = item.text; 433 | if (item.completed) { 434 | // display_text = "✓ " + display_text; 435 | } 436 | 437 | int text_x = (x_offset > 0) ? (abs_offset + 20) : (bg_x + 20); 438 | int text_y = y + item_height / 2 + 6; 439 | 440 | // Draw text 441 | fl_draw(display_text.c_str(), text_x, text_y); 442 | 443 | // Draw strikethrough for completed items 444 | if (item.completed) { 445 | int text_w, text_h; 446 | measure_text(display_text, text_w, text_h, 18); 447 | // Draw strikethrough line with same color as text 448 | fl_line(text_x, text_y - text_h / 2, text_x + text_w, text_y - text_h / 2); 449 | } 450 | } 451 | } 452 | 453 | // Get sorted indices (incomplete first, then completed) 454 | std::vector get_sorted_indices() { 455 | std::vector indices; 456 | // First add incomplete items 457 | for (size_t i = 0; i < items.size(); i++) { 458 | if (!items[i].completed) { 459 | indices.push_back(i); 460 | } 461 | } 462 | // Then add completed items 463 | for (size_t i = 0; i < items.size(); i++) { 464 | if (items[i].completed) { 465 | indices.push_back(i); 466 | } 467 | } 468 | return indices; 469 | } 470 | 471 | int get_item_at_y(int y) { 472 | int start_y = 0; // Start from top 473 | // Adjust y coordinate for scroll offset 474 | int adjusted_y = y + scroll_offset; 475 | if (adjusted_y < start_y) 476 | return -1; // Above first item 477 | int visual_index = (adjusted_y - start_y) / item_height; 478 | 479 | // Get sorted indices to map visual position to actual index 480 | std::vector sorted_indices = get_sorted_indices(); 481 | if (visual_index >= 0 && visual_index < (int)sorted_indices.size()) { 482 | return sorted_indices[visual_index]; 483 | } 484 | return -1; 485 | } 486 | 487 | // Get maximum scroll offset (how far we can scroll down) 488 | int get_max_scroll_offset() { 489 | int total_height = items.size() * item_height; 490 | int visible_height = h() - 40; // Subtract space for instructions at bottom 491 | int max_scroll = total_height - visible_height; 492 | return (max_scroll > 0) ? max_scroll : 0; 493 | } 494 | 495 | // Clamp scroll offset to valid range 496 | void clamp_scroll_offset() { 497 | int max_scroll = get_max_scroll_offset(); 498 | if (scroll_offset < 0) { 499 | scroll_offset = 0; 500 | } else if (scroll_offset > max_scroll) { 501 | scroll_offset = max_scroll; 502 | } 503 | } 504 | 505 | void reorder_items(int from_index, int to_index) { 506 | if (from_index < 0 || from_index >= (int)items.size() || to_index < 0 || 507 | to_index >= (int)items.size() || from_index == to_index) { 508 | return; 509 | } 510 | 511 | TodoItem item = items[from_index]; 512 | items.erase(items.begin() + from_index); 513 | items.insert(items.begin() + to_index, item); 514 | save_to_file(); 515 | redraw(); 516 | } 517 | 518 | public: 519 | ClearApp(int W, int H, const char *title) 520 | : Fl_Window(W, H, title), selected_index(-1), is_dragging(false), 521 | is_swiping(false), is_pulling_down(false), pull_down_offset(0), 522 | drag_offset(0), item_height(60), data_file(""), editing_index(-1), 523 | pending_click_index(-1), can_reorder(false), input_widget(nullptr), 524 | scroll_offset(0) { 525 | 526 | // Initialize data file path to application data directory 527 | std::string data_dir = get_data_directory(); 528 | if (data_dir == ".") { 529 | data_file = "todos.txt"; // Fallback to current directory 530 | } else { 531 | #ifdef _WIN32 532 | data_file = data_dir + "\\todos.txt"; 533 | #else 534 | data_file = data_dir + "/todos.txt"; 535 | #endif 536 | } 537 | 538 | color(fl_rgb_color(64, 64, 64)); // deep gray 539 | 540 | // Create input widget (initially hidden) 541 | input_widget = new Fl_Input(0, 0, w(), item_height); 542 | input_widget->callback(input_callback, this); 543 | input_widget->when(FL_WHEN_CHANGED | FL_WHEN_ENTER_KEY | FL_WHEN_RELEASE); 544 | input_widget->box(FL_FLAT_BOX); // Flat box (background but no border) 545 | input_widget->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE); 546 | // Ensure the widget can receive focus and display properly 547 | input_widget->set_visible_focus(); 548 | input_widget->hide(); 549 | 550 | // Load items from file 551 | bool loaded = load_from_file(); 552 | 553 | // If no items loaded (first run), add sample items 554 | if (!loaded || items.empty()) { 555 | add_sample_items(); 556 | save_to_file(); // Save sample items to file 557 | } 558 | 559 | end(); 560 | } 561 | 562 | ~ClearApp() { save_to_file(); } 563 | 564 | void add_item(const std::string &text = "") { 565 | // Finish any existing editing first 566 | if (editing_index >= 0) { 567 | finish_editing(); 568 | } 569 | 570 | // Reset pull down state to avoid position calculation issues 571 | is_pulling_down = false; 572 | pull_down_offset = 0; 573 | 574 | // Reset scroll to top when adding new item at the beginning 575 | scroll_offset = 0; 576 | 577 | // Insert new item at the beginning 578 | items.insert(items.begin(), TodoItem(text)); 579 | 580 | // Start editing the new item 581 | editing_index = 0; 582 | editing_text = text; 583 | start_editing(0); 584 | 585 | save_to_file(); 586 | } 587 | 588 | void start_editing(int index) { 589 | if (index < 0 || index >= (int)items.size()) { 590 | return; 591 | } 592 | 593 | // Finish any existing editing first 594 | if (editing_index >= 0 && editing_index != index) { 595 | finish_editing(); 596 | } 597 | 598 | editing_index = index; 599 | editing_text = items[index].text; 600 | 601 | // Calculate position for input widget 602 | int start_y = 0; 603 | int item_y = start_y + index * item_height - scroll_offset; 604 | if (is_pulling_down && pull_down_offset > 0) { 605 | item_y += pull_down_offset; 606 | } 607 | 608 | // Adjust for swipe offset 609 | int x_offset = items[index].swipe_offset; 610 | int abs_offset = abs(x_offset); 611 | int bg_x = (x_offset > 0) ? abs_offset : 0; 612 | // Add 20 pixels left padding to match non-editing text position 613 | int input_x = bg_x + 20; 614 | int input_w = w() - abs_offset - 20; 615 | if (input_w < 0) 616 | input_w = 0; 617 | 618 | // Position and show input widget 619 | // Make it cover the full item area, with left padding for text alignment 620 | input_widget->resize(input_x, item_y, input_w, item_height); 621 | 622 | // Set colors based on item position 623 | // For completed items, use dark gray; otherwise use visual position in sorted list 624 | Fl_Color item_color; 625 | if (items[index].completed) { 626 | item_color = fl_rgb_color(64, 64, 64); // Dark gray for completed items 627 | } else { 628 | // Find visual position in sorted list 629 | std::vector sorted_indices = get_sorted_indices(); 630 | int visual_pos = -1; 631 | for (size_t i = 0; i < sorted_indices.size(); i++) { 632 | if (sorted_indices[i] == index) { 633 | visual_pos = i; 634 | break; 635 | } 636 | } 637 | if (visual_pos >= 0) { 638 | item_color = get_color_by_position(visual_pos, sorted_indices.size()); 639 | } else { 640 | item_color = get_color_by_position(index, items.size()); 641 | } 642 | } 643 | Fl_Color text_color = get_text_color(item_color); 644 | 645 | // Set background color 646 | input_widget->color(item_color); 647 | 648 | // Set text color - ensure high contrast and visibility 649 | input_widget->textcolor(text_color); 650 | input_widget->textfont(FL_HELVETICA_BOLD); 651 | input_widget->textsize(18); 652 | 653 | // Set selection color for cursor visibility (use contrasting color) 654 | Fl_Color selection_color = (text_color == FL_BLACK) ? FL_BLUE : FL_YELLOW; 655 | input_widget->selection_color(selection_color); 656 | 657 | // Set value - ensure it's set before showing 658 | std::string text_value = items[index].text; 659 | input_widget->value(text_value.c_str()); 660 | 661 | // Show and activate 662 | input_widget->show(); 663 | input_widget->activate(); 664 | input_widget->set_visible_focus(); 665 | 666 | // Set cursor to end of text 667 | int text_len = input_widget->size(); 668 | if (text_len > 0) { 669 | input_widget->insert_position(text_len); 670 | input_widget->mark(text_len); // Clear any selection 671 | } else { 672 | input_widget->insert_position(0); 673 | input_widget->mark(0); 674 | } 675 | 676 | // Take focus to ensure input works 677 | input_widget->take_focus(); 678 | 679 | // Force complete redraw 680 | input_widget->damage(FL_DAMAGE_ALL); 681 | input_widget->redraw(); 682 | 683 | // Process events to ensure everything is updated 684 | Fl::check(); 685 | Fl::flush(); 686 | redraw(); 687 | } 688 | 689 | void finish_editing() { 690 | if (editing_index >= 0 && editing_index < (int)items.size()) { 691 | // Get text from input widget 692 | if (input_widget && input_widget->visible()) { 693 | editing_text = input_widget->value() ? input_widget->value() : ""; 694 | } 695 | 696 | input_widget->hide(); 697 | 698 | int old_editing_index = editing_index; 699 | std::string old_editing_text = editing_text; 700 | 701 | if (old_editing_text.empty()) { 702 | // Remove empty item, but keep at least one empty item if list becomes 703 | // empty 704 | items.erase(items.begin() + old_editing_index); 705 | if (items.empty()) { 706 | items.push_back(TodoItem("")); 707 | editing_index = -1; // Reset first to avoid recursion 708 | editing_text = ""; 709 | redraw(); 710 | start_editing(0); 711 | return; 712 | } else { 713 | editing_index = -1; 714 | editing_text = ""; 715 | } 716 | } else { 717 | items[old_editing_index].text = old_editing_text; 718 | editing_index = -1; 719 | editing_text = ""; 720 | save_to_file(); 721 | } 722 | redraw(); 723 | } 724 | } 725 | 726 | void delete_item(int index) { 727 | if (index >= 0 && index < (int)items.size()) { 728 | items.erase(items.begin() + index); 729 | if (selected_index >= (int)items.size()) { 730 | selected_index = -1; 731 | } 732 | // Reset swipe offsets for all items 733 | for (auto &item : items) { 734 | item.swipe_offset = 0; 735 | } 736 | 737 | // Clamp scroll offset after deletion 738 | clamp_scroll_offset(); 739 | 740 | // If all items are deleted, add an empty item for input 741 | if (items.empty()) { 742 | items.push_back(TodoItem("")); 743 | editing_index = 0; 744 | editing_text = ""; 745 | scroll_offset = 0; 746 | start_editing(0); 747 | } 748 | 749 | save_to_file(); 750 | redraw(); 751 | } 752 | } 753 | 754 | void toggle_complete(int index) { 755 | if (index >= 0 && index < (int)items.size()) { 756 | items[index].completed = !items[index].completed; 757 | save_to_file(); 758 | redraw(); 759 | } 760 | } 761 | 762 | int handle(int event) override { 763 | int mx = Fl::event_x(); 764 | int my = Fl::event_y(); 765 | int start_y = 0; 766 | 767 | switch (event) { 768 | case FL_PUSH: { 769 | // Finish editing if clicking elsewhere 770 | if (editing_index >= 0 && Fl::event_button() == FL_LEFT_MOUSE) { 771 | int clicked_index = get_item_at_y(my); 772 | if (clicked_index != editing_index) { 773 | finish_editing(); 774 | } 775 | } 776 | 777 | // Reset all swipe offsets when starting new interaction 778 | for (auto &item : items) { 779 | item.swipe_offset = 0; 780 | } 781 | 782 | int index = get_item_at_y(my); 783 | 784 | // Check if we're in the pull-down zone (above all items, not on first 785 | // item) Pull zone is only above the first item, not including the first 786 | // item itself 787 | bool in_pull_zone = (my < start_y && index < 0); 788 | 789 | if (index >= 0) { 790 | // Clicked on an item (including first item) 791 | if (Fl::event_button() == FL_LEFT_MOUSE) { 792 | // Start potential drag - but need long press for reordering 793 | selected_index = index; 794 | is_dragging = false; // Don't allow dragging yet 795 | is_swiping = false; 796 | is_pulling_down = false; 797 | can_reorder = false; // Need long press first 798 | drag_start_y = my; 799 | drag_start_x = mx; 800 | drag_offset = my - (start_y + index * item_height); 801 | pending_click_index = -1; // Reset pending click 802 | 803 | // Start long press timer (0.3 seconds) for reordering 804 | Fl::add_timeout(0.3, long_press_timeout_cb, this); 805 | redraw(); 806 | } else if (Fl::event_button() == FL_RIGHT_MOUSE) { 807 | // Right-click to delete 808 | if (editing_index >= 0) { 809 | finish_editing(); 810 | } 811 | delete_item(index); 812 | } 813 | } else if (in_pull_zone && Fl::event_button() == FL_LEFT_MOUSE) { 814 | // Click in pull-down zone (above all items) - start pull down 815 | is_dragging = true; 816 | is_pulling_down = true; 817 | drag_start_y = my; 818 | drag_start_x = mx; 819 | pull_down_offset = 0; 820 | selected_index = -1; 821 | redraw(); 822 | } 823 | return 1; 824 | } 825 | 826 | case FL_DRAG: { 827 | int dx = mx - drag_start_x; 828 | int dy = my - drag_start_y; 829 | 830 | if (is_pulling_down) { 831 | // Pull down gesture - immediate response, no long press needed 832 | if (dy > 0) { 833 | pull_down_offset = dy; 834 | if (pull_down_offset > item_height * 1.5) { 835 | pull_down_offset = item_height * 1.5; 836 | } 837 | redraw(); 838 | } else if (dy < -5) { 839 | // Pulled back up significantly, cancel 840 | pull_down_offset = 0; 841 | redraw(); 842 | } 843 | } else if (selected_index >= 0) { 844 | // Check if dragging down to add new item (immediate, no long press 845 | // needed) Only allow this if dragging down significantly and not 846 | // swiping horizontally 847 | if (!can_reorder && dy > 20 && abs(dx) < 30) { 848 | // Dragging down - switch to pull-down mode to add new item 849 | Fl::remove_timeout(long_press_timeout_cb, this); 850 | is_pulling_down = true; 851 | is_dragging = true; 852 | selected_index = -1; 853 | pull_down_offset = dy; 854 | redraw(); 855 | } else if (!can_reorder) { 856 | // Check if this is a horizontal swipe (for complete or delete) 857 | if (abs(dx) > 10 && abs(dx) > abs(dy)) { 858 | // Horizontal swipe - allow it immediately 859 | is_swiping = true; 860 | is_dragging = true; 861 | items[selected_index].swipe_offset = 862 | dx; // Can be positive (right) or negative (left) 863 | redraw(); 864 | } else if (abs(dx) > 5 || abs(dy) > 5) { 865 | // Moved but not dragging down or swiping - cancel long press timer 866 | Fl::remove_timeout(long_press_timeout_cb, this); 867 | can_reorder = false; 868 | // Don't allow dragging yet 869 | return 1; 870 | } else { 871 | // Small movement, wait for long press 872 | return 1; 873 | } 874 | } 875 | 876 | // Only allow reordering after long press 877 | if (!can_reorder && !is_swiping) { 878 | return 1; 879 | } 880 | 881 | // Now we can drag for reordering or swiping 882 | if (!is_swiping) { 883 | is_dragging = true; 884 | } 885 | 886 | // Determine if this is a horizontal swipe or vertical drag 887 | if (!is_swiping && abs(dx) > 10) { 888 | is_swiping = true; 889 | } 890 | 891 | if (is_swiping) { 892 | // Horizontal swipe - can be right (complete) or left (delete) 893 | items[selected_index].swipe_offset = dx; 894 | redraw(); 895 | } else if (can_reorder && abs(dy) > 10) { 896 | // Vertical drag for reordering (only after long press) 897 | int new_index = get_item_at_y(my); 898 | if (new_index >= 0 && new_index != selected_index) { 899 | reorder_items(selected_index, new_index); 900 | selected_index = new_index; 901 | } 902 | redraw(); 903 | } 904 | } 905 | return 1; 906 | } 907 | 908 | case FL_RELEASE: { 909 | // Cancel long press timer if still waiting 910 | Fl::remove_timeout(long_press_timeout_cb, this); 911 | 912 | if (is_pulling_down) { 913 | // If pulled down enough, create new item 914 | if (pull_down_offset > item_height * 0.6) { 915 | // Add new item (this will reset pull down state and start editing) 916 | add_item(""); 917 | } else { 918 | pull_down_offset = 0; 919 | is_pulling_down = false; 920 | } 921 | } else if (is_dragging && selected_index >= 0 && can_reorder) { 922 | if (is_swiping) { 923 | // Handle swipe based on direction 924 | int swipe_offset = items[selected_index].swipe_offset; 925 | if (swipe_offset < -w() * 0.3) { 926 | // Swiped left far enough - delete 927 | if (editing_index == selected_index) { 928 | editing_index = -1; 929 | editing_text = ""; 930 | } 931 | delete_item(selected_index); 932 | } else if (swipe_offset > w() * 0.3) { 933 | // Swiped right far enough - complete 934 | toggle_complete(selected_index); 935 | items[selected_index].swipe_offset = 0; 936 | } else { 937 | // Snap back 938 | items[selected_index].swipe_offset = 0; 939 | } 940 | } 941 | } else if (selected_index >= 0) { 942 | // Released without long press or without dragging 943 | int dx = mx - drag_start_x; 944 | int dy = my - drag_start_y; 945 | 946 | if (is_swiping) { 947 | // Handle swipe based on direction 948 | int swipe_offset = items[selected_index].swipe_offset; 949 | if (swipe_offset < -w() * 0.3) { 950 | // Swiped left far enough - delete 951 | if (editing_index == selected_index) { 952 | editing_index = -1; 953 | editing_text = ""; 954 | } 955 | delete_item(selected_index); 956 | } else if (swipe_offset > w() * 0.3) { 957 | // Swiped right far enough - complete 958 | toggle_complete(selected_index); 959 | items[selected_index].swipe_offset = 0; 960 | } else { 961 | // Snap back 962 | items[selected_index].swipe_offset = 0; 963 | } 964 | } else if (abs(dx) < 5 && abs(dy) < 5 && !can_reorder) { 965 | // Small movement - treat as click 966 | // Check if this is a double-click 967 | if (Fl::event_clicks() > 0) { 968 | // Double-click - toggle complete 969 | // Cancel any pending single click 970 | if (pending_click_index >= 0) { 971 | Fl::remove_timeout(click_timeout_cb, this); 972 | pending_click_index = -1; 973 | } 974 | if (editing_index < 0) { 975 | toggle_complete(selected_index); 976 | } 977 | } else { 978 | // Single click - delay to check for double-click 979 | // Cancel previous pending click if any 980 | if (pending_click_index >= 0) { 981 | Fl::remove_timeout(click_timeout_cb, this); 982 | } 983 | pending_click_index = selected_index; 984 | Fl::add_timeout(0.3, click_timeout_cb, this); 985 | } 986 | } 987 | } 988 | 989 | is_dragging = false; 990 | is_swiping = false; 991 | can_reorder = false; 992 | redraw(); 993 | return 1; 994 | } 995 | 996 | case FL_MOUSEWHEEL: { 997 | // Handle mouse wheel scrolling 998 | int dy = Fl::event_dy(); 999 | if (dy != 0) { 1000 | scroll_offset -= dy * item_height; // Negative because scrolling down 1001 | // should increase offset 1002 | clamp_scroll_offset(); 1003 | redraw(); 1004 | return 1; 1005 | } 1006 | break; 1007 | } 1008 | 1009 | case FL_KEYBOARD: { 1010 | // If editing, only handle Escape key, let Fl_Input handle everything else 1011 | if (editing_index >= 0 && input_widget && input_widget->visible()) { 1012 | int key = Fl::event_key(); 1013 | if (key == FL_Escape) { 1014 | // Cancel editing 1015 | std::string current_text = 1016 | input_widget->value() ? input_widget->value() : ""; 1017 | if (current_text.empty() && editing_index < (int)items.size()) { 1018 | items.erase(items.begin() + editing_index); 1019 | if (items.empty()) { 1020 | items.push_back(TodoItem("")); 1021 | editing_index = 0; 1022 | editing_text = ""; 1023 | start_editing(0); 1024 | return 1; 1025 | } 1026 | } 1027 | input_widget->hide(); 1028 | editing_index = -1; 1029 | editing_text = ""; 1030 | save_to_file(); 1031 | redraw(); 1032 | return 1; 1033 | } 1034 | // For all other keys, don't intercept - let Fl_Input handle them 1035 | // Call the parent handle to let event propagate naturally 1036 | break; // Don't handle, let it fall through to parent or Fl_Input 1037 | } else if (Fl::event_key() == FL_Delete && selected_index >= 0) { 1038 | delete_item(selected_index); 1039 | selected_index = -1; 1040 | return 1; 1041 | } 1042 | break; 1043 | } 1044 | } 1045 | 1046 | return Fl_Window::handle(event); 1047 | } 1048 | 1049 | void draw() override { 1050 | Fl_Window::draw(); 1051 | 1052 | int start_y = 0; 1053 | int y = start_y; 1054 | 1055 | // Draw pull-down new item if pulling 1056 | if (is_pulling_down && pull_down_offset > 0) { 1057 | int new_item_y = start_y - item_height + pull_down_offset - scroll_offset; 1058 | // Only draw if visible 1059 | if (new_item_y + item_height > 0 && new_item_y < h()) { 1060 | // Draw the new item being pulled down 1061 | // New item will be at position 0, so use red color 1062 | Fl_Color new_color = get_color_by_position(0, items.size() + 1); 1063 | fl_color(new_color); 1064 | fl_rectf(0, new_item_y, w(), item_height); 1065 | 1066 | // Use appropriate text color based on background 1067 | Fl_Color text_color = get_text_color(new_color); 1068 | fl_color(text_color); 1069 | fl_font(FL_HELVETICA_BOLD, 18); 1070 | if (pull_down_offset > item_height * 0.6) { 1071 | fl_draw("Release to add...", 20, new_item_y + item_height / 2 + 6); 1072 | } else { 1073 | fl_draw("Pull down to add...", 20, new_item_y + item_height / 2 + 6); 1074 | } 1075 | } 1076 | } 1077 | 1078 | // Get sorted indices (incomplete first, then completed) 1079 | std::vector sorted_indices = get_sorted_indices(); 1080 | 1081 | // Draw all items (shift down when pulling, adjust for scroll) 1082 | // Use sorted indices so completed items appear at bottom 1083 | for (size_t visual_pos = 0; visual_pos < sorted_indices.size(); visual_pos++) { 1084 | int actual_index = sorted_indices[visual_pos]; 1085 | int item_y = y - scroll_offset; 1086 | if (is_pulling_down && pull_down_offset > 0) { 1087 | // Shift all items down when pulling 1088 | item_y += pull_down_offset; 1089 | } 1090 | // Only draw if item is visible (optimization) 1091 | if (item_y + item_height > 0 && item_y < h()) { 1092 | // Pass visual position for color calculation, but use actual index for item data 1093 | draw_item(actual_index, item_y, (actual_index == editing_index), visual_pos, sorted_indices.size()); 1094 | } 1095 | y += item_height; 1096 | } 1097 | 1098 | // Update Fl_Input position if editing 1099 | if (editing_index >= 0 && editing_index < (int)items.size() && 1100 | input_widget && input_widget->visible()) { 1101 | int start_y = 0; 1102 | // Find visual position of editing item in sorted list 1103 | int visual_pos = -1; 1104 | for (size_t i = 0; i < sorted_indices.size(); i++) { 1105 | if (sorted_indices[i] == editing_index) { 1106 | visual_pos = i; 1107 | break; 1108 | } 1109 | } 1110 | int item_y = start_y; 1111 | if (visual_pos >= 0) { 1112 | item_y = start_y + visual_pos * item_height - scroll_offset; 1113 | } else { 1114 | // Fallback to old calculation if not found in sorted list 1115 | item_y = start_y + editing_index * item_height - scroll_offset; 1116 | } 1117 | if (is_pulling_down && pull_down_offset > 0) { 1118 | item_y += pull_down_offset; 1119 | } 1120 | 1121 | // Ensure editing item is visible by scrolling if needed 1122 | if (item_y < 0) { 1123 | scroll_offset += item_y; 1124 | clamp_scroll_offset(); 1125 | if (visual_pos >= 0) { 1126 | item_y = start_y + visual_pos * item_height - scroll_offset; 1127 | } else { 1128 | item_y = start_y + editing_index * item_height - scroll_offset; 1129 | } 1130 | if (is_pulling_down && pull_down_offset > 0) { 1131 | item_y += pull_down_offset; 1132 | } 1133 | } else if (item_y + item_height > h() - 40) { 1134 | scroll_offset += (item_y + item_height) - (h() - 40); 1135 | clamp_scroll_offset(); 1136 | if (visual_pos >= 0) { 1137 | item_y = start_y + visual_pos * item_height - scroll_offset; 1138 | } else { 1139 | item_y = start_y + editing_index * item_height - scroll_offset; 1140 | } 1141 | if (is_pulling_down && pull_down_offset > 0) { 1142 | item_y += pull_down_offset; 1143 | } 1144 | } 1145 | 1146 | // Adjust for swipe offset 1147 | int x_offset = items[editing_index].swipe_offset; 1148 | int abs_offset = abs(x_offset); 1149 | int bg_x = (x_offset > 0) ? abs_offset : 0; 1150 | // Add 20 pixels left padding to match non-editing text position 1151 | int input_x = bg_x + 20; 1152 | int input_w = w() - abs_offset - 20; 1153 | if (input_w < 0) 1154 | input_w = 0; 1155 | 1156 | // Update position and colors 1157 | input_widget->resize(input_x, item_y, input_w, item_height); 1158 | 1159 | // Use visual position for color if available, otherwise use actual index 1160 | Fl_Color item_color; 1161 | if (items[editing_index].completed) { 1162 | item_color = fl_rgb_color(64, 64, 64); // Dark gray for completed items 1163 | } else if (visual_pos >= 0) { 1164 | item_color = get_color_by_position(visual_pos, sorted_indices.size()); 1165 | } else { 1166 | item_color = get_color_by_position(editing_index, items.size()); 1167 | } 1168 | Fl_Color text_color = get_text_color(item_color); 1169 | input_widget->color(item_color); 1170 | input_widget->textcolor(text_color); 1171 | // Set selection color for cursor visibility 1172 | Fl_Color selection_color = (text_color == FL_BLACK) ? FL_BLUE : FL_YELLOW; 1173 | input_widget->selection_color(selection_color); 1174 | // Force redraw 1175 | input_widget->redraw(); 1176 | } 1177 | 1178 | // Draw instructions at bottom 1179 | fl_color(FL_WHITE); 1180 | fl_font(FL_HELVETICA, 12); 1181 | fl_draw("Pull down to add | Click to edit | Double-click to complete | " 1182 | "Swipe right to delete", 1183 | 10, h() - 20); 1184 | 1185 | // Draw error message in bottom right corner 1186 | if (error_display.is_visible && !error_display.message.empty()) { 1187 | const int font_size = 14; 1188 | const int padding = 12; 1189 | const int margin = 10; 1190 | const int corner_radius = 8; 1191 | 1192 | fl_font(FL_HELVETICA_BOLD, font_size); 1193 | 1194 | // Measure text accurately 1195 | int text_w, text_h; 1196 | measure_text(error_display.message, text_w, text_h, font_size); 1197 | 1198 | // Calculate box dimensions with padding 1199 | int box_w = text_w + padding * 2; 1200 | int box_h = text_h + padding * 2; 1201 | 1202 | // Ensure minimum width for better appearance 1203 | if (box_w < 200) 1204 | box_w = 200; 1205 | 1206 | // Position in bottom right corner (above instructions) 1207 | int box_x = w() - box_w - margin; 1208 | int box_y = h() - box_h - 35; // Above the instruction text 1209 | 1210 | // Save current drawing state 1211 | fl_push_clip(box_x, box_y, box_w, box_h); 1212 | 1213 | // Draw shadow for depth (offset by 2 pixels) with rounded corners 1214 | fl_color(fl_rgb_color(20, 20, 20)); // Dark gray for shadow effect 1215 | draw_rounded_rect(box_x + 2, box_y + 2, box_w, box_h, corner_radius); 1216 | 1217 | // Draw rounded rectangle background with 90% opacity (10% transparency) 1218 | // For 90% opacity on white background (255,255,255): 1219 | // Target color = base_color * 0.9 + white * 0.1 1220 | // If we want final color around (60,60,60): 1221 | // base_color = (60 - 255*0.1) / 0.9 ≈ 38 1222 | // Final = 38*0.9 + 255*0.1 = 34.2 + 25.5 ≈ 60 1223 | // Using base color (40,40,40) for better visibility: 1224 | // Final = 40*0.9 + 255*0.1 = 36 + 25.5 ≈ 62 1225 | fl_color(fl_rgb_color(62, 62, 62)); 1226 | draw_rounded_rect(box_x, box_y, box_w, box_h, corner_radius); 1227 | 1228 | // Restore clipping 1229 | fl_pop_clip(); 1230 | 1231 | // Draw error text in white 1232 | fl_color(FL_WHITE); 1233 | fl_draw(error_display.message.c_str(), box_x + padding, 1234 | box_y + padding + text_h - 4); 1235 | } 1236 | } 1237 | 1238 | void handle_single_click(int index) { 1239 | if (index >= 0 && index < (int)items.size() && 1240 | pending_click_index == index) { 1241 | // This is a single click (not a double-click), start editing 1242 | if (editing_index != index) { 1243 | start_editing(index); 1244 | } 1245 | pending_click_index = -1; 1246 | } 1247 | } 1248 | 1249 | void enable_reorder() { 1250 | if (selected_index >= 0) { 1251 | can_reorder = true; 1252 | is_dragging = true; 1253 | redraw(); 1254 | } 1255 | } 1256 | 1257 | static void input_callback(Fl_Widget *widget, void *data) { 1258 | ClearApp *app = static_cast(data); 1259 | Fl_Input *input = app->input_widget; 1260 | if (!input || !input->visible()) 1261 | return; 1262 | 1263 | // Check if Enter was pressed 1264 | int key = Fl::event_key(); 1265 | if (key == FL_Enter || key == FL_KP_Enter) { 1266 | app->finish_editing(); 1267 | return; 1268 | } 1269 | 1270 | // Text changed - force immediate redraw to show input in real-time 1271 | input->damage(FL_DAMAGE_ALL); 1272 | input->redraw(); 1273 | // Process events immediately to update display 1274 | Fl::check(); 1275 | Fl::flush(); 1276 | } 1277 | 1278 | static void click_timeout_cb(void *data) { 1279 | ClearApp *app = static_cast(data); 1280 | if (app->pending_click_index >= 0) { 1281 | app->handle_single_click(app->pending_click_index); 1282 | } 1283 | } 1284 | 1285 | static void long_press_timeout_cb(void *data) { 1286 | ClearApp *app = static_cast(data); 1287 | app->enable_reorder(); 1288 | } 1289 | }; 1290 | 1291 | int main(int argc, char **argv) { 1292 | ClearApp *app = 1293 | new ClearApp(600, 800, "Clear-txt - Todo List with .txt file."); 1294 | app->show(argc, argv); 1295 | return Fl::run(); 1296 | } 1297 | --------------------------------------------------------------------------------