├── README.md
├── clear.png
├── Info.plist
├── Makefile
├── create_icon.sh
└── clear.cc
/README.md:
--------------------------------------------------------------------------------
1 | # Clear-txt
2 |
3 | 
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 |
--------------------------------------------------------------------------------