How to make a custom composite field?
Except the value never gets saved, and I can't process the input text
in hook_entity_presave().

You can add a new validate for your widget:
/**
* {@inheritdoc}
*/
public function formElement(FieldItemListInterface $items, $delta, array $element, array &amp;$form, FormStateInterface $form_state) {
  // Your logic code

  // Add validate for your widget.
  $element['#element_validate'] = [
    [$this, 'validate'],
  ];

  return $element;
}

And convert value before save:
/**
* Validate the fields and convert data before save.
*/
public function validate($element, FormStateInterface $form_state) {
    // Logic validate.
    // Code here

    // Convert value and set.
    // Field type text_long
    $value = [
     'value' =&gt; 'your value',
     'format' =&gt; 'your format'
    ];

    // Field type string_long
    $value = [
      'value' =&gt; 'your value'
    ];

    $form_state-&gt;setValueForElement($element, $value);
}