diff --git a/CHANGELOG.md b/CHANGELOG.md
index b620ebcee40f..45179dbc7b28 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,7 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui_w
 * Added `Frame::outer_margin`.
 * Added `Painter::hline` and `Painter::vline`.
 * Added `Link` and `ui.link`  ([#1506](https://github.com/emilk/egui/pull/1506)).
+* Added triple-click support; triple-clicking a TextEdit field will select the whole paragraph ([#1512](https://github.com/emilk/egui/pull/1512)).
 * Added `Plot::x_grid_spacer` and `Plot::y_grid_spacer` for custom grid spacing ([#1180](https://github.com/emilk/egui/pull/1180)).
 
 ### Changed 🔧
diff --git a/egui/src/context.rs b/egui/src/context.rs
index aaf282638539..1c3e50b8bba1 100644
--- a/egui/src/context.rs
+++ b/egui/src/context.rs
@@ -327,6 +327,7 @@ impl Context {
             hovered,
             clicked: Default::default(),
             double_clicked: Default::default(),
+            triple_clicked: Default::default(),
             dragged: false,
             drag_released: false,
             is_pointer_button_down_on: false,
@@ -410,6 +411,8 @@ impl Context {
                                 response.clicked[click.button as usize] = clicked;
                                 response.double_clicked[click.button as usize] =
                                     clicked && click.is_double();
+                                response.triple_clicked[click.button as usize] =
+                                    clicked && click.is_triple();
                             }
                         }
                     }
diff --git a/egui/src/data/output.rs b/egui/src/data/output.rs
index 8f826cd25b1f..0688b59c6e40 100644
--- a/egui/src/data/output.rs
+++ b/egui/src/data/output.rs
@@ -88,6 +88,7 @@ impl PlatformOutput {
             match event {
                 OutputEvent::Clicked(widget_info)
                 | OutputEvent::DoubleClicked(widget_info)
+                | OutputEvent::TripleClicked(widget_info)
                 | OutputEvent::FocusGained(widget_info)
                 | OutputEvent::TextSelectionChanged(widget_info)
                 | OutputEvent::ValueChanged(widget_info) => {
@@ -291,6 +292,8 @@ pub enum OutputEvent {
     Clicked(WidgetInfo),
     // A widget was double-clicked.
     DoubleClicked(WidgetInfo),
+    // A widget was triple-clicked.
+    TripleClicked(WidgetInfo),
     /// A widget gained keyboard focus (by tab key).
     FocusGained(WidgetInfo),
     // Text selection was updated.
@@ -304,6 +307,7 @@ impl std::fmt::Debug for OutputEvent {
         match self {
             Self::Clicked(wi) => write!(f, "Clicked({:?})", wi),
             Self::DoubleClicked(wi) => write!(f, "DoubleClicked({:?})", wi),
+            Self::TripleClicked(wi) => write!(f, "TripleClicked({:?})", wi),
             Self::FocusGained(wi) => write!(f, "FocusGained({:?})", wi),
             Self::TextSelectionChanged(wi) => write!(f, "TextSelectionChanged({:?})", wi),
             Self::ValueChanged(wi) => write!(f, "ValueChanged({:?})", wi),
diff --git a/egui/src/input_state.rs b/egui/src/input_state.rs
index 440a3c44cb5b..4faec5c3ff8a 100644
--- a/egui/src/input_state.rs
+++ b/egui/src/input_state.rs
@@ -349,7 +349,7 @@ impl InputState {
 pub(crate) struct Click {
     pub pos: Pos2,
     pub button: PointerButton,
-    /// 1 or 2 (double-click)
+    /// 1 or 2 (double-click) or 3 (triple-click)
     pub count: u32,
     /// Allows you to check for e.g. shift-click
     pub modifiers: Modifiers,
@@ -359,6 +359,9 @@ impl Click {
     pub fn is_double(&self) -> bool {
         self.count == 2
     }
+    pub fn is_triple(&self) -> bool {
+        self.count == 3
+    }
 }
 
 #[derive(Clone, Debug, PartialEq)]
@@ -429,6 +432,10 @@ pub struct PointerState {
     /// Used to check for double-clicks.
     last_click_time: f64,
 
+    /// When did the pointer get click two clicks ago?
+    /// Used to check for triple-clicks.
+    last_last_click_time: f64,
+
     /// All button events that occurred this frame
     pub(crate) pointer_events: Vec<PointerEvent>,
 }
@@ -447,6 +454,7 @@ impl Default for PointerState {
             press_start_time: None,
             has_moved_too_much_for_a_click: false,
             last_click_time: std::f64::NEG_INFINITY,
+            last_last_click_time: std::f64::NEG_INFINITY,
             pointer_events: vec![],
         }
     }
@@ -508,8 +516,17 @@ impl PointerState {
                         let click = if clicked {
                             let double_click =
                                 (time - self.last_click_time) < MAX_DOUBLE_CLICK_DELAY;
-                            let count = if double_click { 2 } else { 1 };
-
+                            let triple_click =
+                                (time - self.last_last_click_time) < (MAX_DOUBLE_CLICK_DELAY * 2.0);
+                            let count = if triple_click {
+                                3
+                            } else if double_click {
+                                2
+                            } else {
+                                1
+                            };
+
+                            self.last_last_click_time = self.last_click_time;
                             self.last_click_time = time;
 
                             Some(Click {
@@ -797,6 +814,7 @@ impl PointerState {
             press_start_time,
             has_moved_too_much_for_a_click,
             last_click_time,
+            last_last_click_time,
             pointer_events,
         } = self;
 
@@ -815,6 +833,7 @@ impl PointerState {
             has_moved_too_much_for_a_click
         ));
         ui.label(format!("last_click_time: {:#?}", last_click_time));
+        ui.label(format!("last_last_click_time: {:#?}", last_last_click_time));
         ui.label(format!("pointer_events: {:?}", pointer_events));
     }
 }
diff --git a/egui/src/response.rs b/egui/src/response.rs
index b6390081a226..df79b6902045 100644
--- a/egui/src/response.rs
+++ b/egui/src/response.rs
@@ -47,6 +47,9 @@ pub struct Response {
     /// The thing was double-clicked.
     pub(crate) double_clicked: [bool; NUM_POINTER_BUTTONS],
 
+    /// The thing was triple-clicked.
+    pub(crate) triple_clicked: [bool; NUM_POINTER_BUTTONS],
+
     /// The widgets is being dragged
     pub(crate) dragged: bool,
 
@@ -79,6 +82,7 @@ impl std::fmt::Debug for Response {
             hovered,
             clicked,
             double_clicked,
+            triple_clicked,
             dragged,
             drag_released,
             is_pointer_button_down_on,
@@ -94,6 +98,7 @@ impl std::fmt::Debug for Response {
             .field("hovered", hovered)
             .field("clicked", clicked)
             .field("double_clicked", double_clicked)
+            .field("triple_clicked", triple_clicked)
             .field("dragged", dragged)
             .field("drag_released", drag_released)
             .field("is_pointer_button_down_on", is_pointer_button_down_on)
@@ -138,11 +143,21 @@ impl Response {
         self.double_clicked[PointerButton::Primary as usize]
     }
 
+    /// Returns true if this widget was triple-clicked this frame by the primary button.
+    pub fn triple_clicked(&self) -> bool {
+        self.triple_clicked[PointerButton::Primary as usize]
+    }
+
     /// Returns true if this widget was double-clicked this frame by the given button.
     pub fn double_clicked_by(&self, button: PointerButton) -> bool {
         self.double_clicked[button as usize]
     }
 
+    /// Returns true if this widget was triple-clicked this frame by the given button.
+    pub fn triple_clicked_by(&self, button: PointerButton) -> bool {
+        self.triple_clicked[button as usize]
+    }
+
     /// `true` if there was a click *outside* this widget this frame.
     pub fn clicked_elsewhere(&self) -> bool {
         // We do not use self.clicked(), because we want to catch all clicks within our frame,
@@ -475,6 +490,8 @@ impl Response {
             Some(OutputEvent::Clicked(make_info()))
         } else if self.double_clicked() {
             Some(OutputEvent::DoubleClicked(make_info()))
+        } else if self.triple_clicked() {
+            Some(OutputEvent::TripleClicked(make_info()))
         } else if self.gained_focus() {
             Some(OutputEvent::FocusGained(make_info()))
         } else if self.changed {
@@ -536,6 +553,11 @@ impl Response {
                 self.double_clicked[1] || other.double_clicked[1],
                 self.double_clicked[2] || other.double_clicked[2],
             ],
+            triple_clicked: [
+                self.triple_clicked[0] || other.triple_clicked[0],
+                self.triple_clicked[1] || other.triple_clicked[1],
+                self.triple_clicked[2] || other.triple_clicked[2],
+            ],
             dragged: self.dragged || other.dragged,
             drag_released: self.drag_released || other.drag_released,
             is_pointer_button_down_on: self.is_pointer_button_down_on
diff --git a/egui/src/widgets/text_edit/builder.rs b/egui/src/widgets/text_edit/builder.rs
index f65c2f603a18..f92a6f910254 100644
--- a/egui/src/widgets/text_edit/builder.rs
+++ b/egui/src/widgets/text_edit/builder.rs
@@ -430,7 +430,6 @@ impl<'t> TextEdit<'t> {
                     ui.output().mutable_text_under_cursor = true;
                 }
 
-                // TODO: triple-click to select whole paragraph
                 // TODO: drag selected text to either move or clone (ctrl on windows, alt on mac)
                 let singleline_offset = vec2(state.singleline_offset, 0.0);
                 let cursor_at_pointer =
@@ -459,6 +458,14 @@ impl<'t> TextEdit<'t> {
                         primary: galley.from_ccursor(ccursor_range.primary),
                         secondary: galley.from_ccursor(ccursor_range.secondary),
                     }));
+                } else if response.triple_clicked() {
+                    // Select line:
+                    let center = cursor_at_pointer;
+                    let ccursor_range = select_line_at(text.as_ref(), center.ccursor);
+                    state.set_cursor_range(Some(CursorRange {
+                        primary: galley.from_ccursor(ccursor_range.primary),
+                        secondary: galley.from_ccursor(ccursor_range.secondary),
+                    }));
                 } else if allow_drag_to_select {
                     if response.hovered() && ui.input().pointer.any_pressed() {
                         ui.memory().request_focus(id);
@@ -1216,6 +1223,41 @@ fn select_word_at(text: &str, ccursor: CCursor) -> CCursorRange {
     }
 }
 
+fn select_line_at(text: &str, ccursor: CCursor) -> CCursorRange {
+    if ccursor.index == 0 {
+        CCursorRange::two(ccursor, ccursor_next_line(text, ccursor))
+    } else {
+        let it = text.chars();
+        let mut it = it.skip(ccursor.index - 1);
+        if let Some(char_before_cursor) = it.next() {
+            if let Some(char_after_cursor) = it.next() {
+                if (!is_linebreak(char_before_cursor)) && (!is_linebreak(char_after_cursor)) {
+                    let min = ccursor_previous_line(text, ccursor + 1);
+                    let max = ccursor_next_line(text, min);
+                    CCursorRange::two(min, max)
+                } else if !is_linebreak(char_before_cursor) {
+                    let min = ccursor_previous_line(text, ccursor);
+                    let max = ccursor_next_line(text, min);
+                    CCursorRange::two(min, max)
+                } else if !is_linebreak(char_after_cursor) {
+                    let max = ccursor_next_line(text, ccursor);
+                    CCursorRange::two(ccursor, max)
+                } else {
+                    let min = ccursor_previous_line(text, ccursor);
+                    let max = ccursor_next_line(text, ccursor);
+                    CCursorRange::two(min, max)
+                }
+            } else {
+                let min = ccursor_previous_line(text, ccursor);
+                CCursorRange::two(min, ccursor)
+            }
+        } else {
+            let max = ccursor_next_line(text, ccursor);
+            CCursorRange::two(ccursor, max)
+        }
+    }
+}
+
 fn ccursor_next_word(text: &str, ccursor: CCursor) -> CCursor {
     CCursor {
         index: next_word_boundary_char_index(text.chars(), ccursor.index),
@@ -1223,6 +1265,13 @@ fn ccursor_next_word(text: &str, ccursor: CCursor) -> CCursor {
     }
 }
 
+fn ccursor_next_line(text: &str, ccursor: CCursor) -> CCursor {
+    CCursor {
+        index: next_line_boundary_char_index(text.chars(), ccursor.index),
+        prefer_next_row: false,
+    }
+}
+
 fn ccursor_previous_word(text: &str, ccursor: CCursor) -> CCursor {
     let num_chars = text.chars().count();
     CCursor {
@@ -1232,6 +1281,15 @@ fn ccursor_previous_word(text: &str, ccursor: CCursor) -> CCursor {
     }
 }
 
+fn ccursor_previous_line(text: &str, ccursor: CCursor) -> CCursor {
+    let num_chars = text.chars().count();
+    CCursor {
+        index: num_chars
+            - next_line_boundary_char_index(text.chars().rev(), num_chars - ccursor.index),
+        prefer_next_row: true,
+    }
+}
+
 fn next_word_boundary_char_index(it: impl Iterator<Item = char>, mut index: usize) -> usize {
     let mut it = it.skip(index);
     if let Some(_first) = it.next() {
@@ -1250,10 +1308,32 @@ fn next_word_boundary_char_index(it: impl Iterator<Item = char>, mut index: usiz
     index
 }
 
+fn next_line_boundary_char_index(it: impl Iterator<Item = char>, mut index: usize) -> usize {
+    let mut it = it.skip(index);
+    if let Some(_first) = it.next() {
+        index += 1;
+
+        if let Some(second) = it.next() {
+            index += 1;
+            for next in it {
+                if is_linebreak(next) != is_linebreak(second) {
+                    break;
+                }
+                index += 1;
+            }
+        }
+    }
+    index
+}
+
 fn is_word_char(c: char) -> bool {
     c.is_ascii_alphanumeric() || c == '_'
 }
 
+fn is_linebreak(c: char) -> bool {
+    c == '\r' || c == '\n'
+}
+
 /// Accepts and returns character offset (NOT byte offset!).
 fn find_line_start(text: &str, current_index: CCursor) -> CCursor {
     // We know that new lines, '\n', are a single byte char, but we have to
diff --git a/egui_demo_lib/src/apps/demo/tests.rs b/egui_demo_lib/src/apps/demo/tests.rs
index f6335cce7c9f..d5d6d7999e04 100644
--- a/egui_demo_lib/src/apps/demo/tests.rs
+++ b/egui_demo_lib/src/apps/demo/tests.rs
@@ -332,7 +332,7 @@ impl super::View for InputTest {
         });
 
         let response = ui.add(
-            egui::Button::new("Click, double-click or drag me with any mouse button")
+            egui::Button::new("Click, double-click, triple-click or drag me with any mouse button")
                 .sense(egui::Sense::click_and_drag()),
         );
 
@@ -348,6 +348,9 @@ impl super::View for InputTest {
             if response.double_clicked_by(button) {
                 new_info += &format!("Double-clicked by {:?} button\n", button);
             }
+            if response.triple_clicked_by(button) {
+                new_info += &format!("Triple-clicked by {:?} button\n", button);
+            }
             if response.dragged_by(button) {
                 new_info += &format!(
                     "Dragged by {:?} button, delta: {:?}\n",